Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d598412
♻️[Refactor] avatar xl 사이즈 추가 #141
haegu97 Dec 12, 2024
b7125f3
✨[Feat] 수정하기 아이콘 추가 #141
haegu97 Dec 12, 2024
9cde451
♻️[Refactor] className을 prop으로 받아 커스텀 디자인 할 수 있게 리팩토링 #141
haegu97 Dec 12, 2024
554d9fc
✨[Feat] Modal 컴포넌트 개발 #141
haegu97 Dec 12, 2024
74e6b14
✨[Feat] 프로필 수정 모달 개발 #141
haegu97 Dec 12, 2024
fdbc373
♻️[Refactor] 오타 수정 #141
haegu97 Dec 12, 2024
47fe3a1
🚚[Rename] 프로필 수정 모달 위치 변경 #141
haegu97 Dec 12, 2024
a79dcf8
✅[Test] 테스트코드, storybook 작성 #141
haegu97 Dec 12, 2024
73a6532
Merge remote-tracking branch 'origin/develop' into 141-refactor-popup…
haegu97 Dec 12, 2024
b577686
💄[Design] 추가된 색상으로 변경 #141
haegu97 Dec 12, 2024
038f7c1
💄[Design] margin 추가 #141
haegu97 Dec 12, 2024
b1ae509
💄[Design] 제목 텍스트 크기 변경 #141
haegu97 Dec 12, 2024
5f0f647
♻️[Refactor] 코드 위치 가독성 좋게 수정 #141
haegu97 Dec 12, 2024
3000eb5
✨[Feat] 리뷰 평점 하트 개발 #141
haegu97 Dec 12, 2024
540dcc3
✨[Feat] 리뷰 작성 모달 개발 #141
haegu97 Dec 12, 2024
a71846b
✅[Test] 스토리북 수정 #141
haegu97 Dec 12, 2024
72b1981
♻️[Refactor] 닫기 버튼 icClose로 변경 #141
haegu97 Dec 13, 2024
923e032
♻️[Refactor] 하트 아이콘 ratingIcon으로 변경 #141
haegu97 Dec 13, 2024
91580e0
🔥[Remove] 리뷰하트아이콘 삭제 #141
haegu97 Dec 13, 2024
6254062
✅[Test] 테스트 코드 수정 #141
haegu97 Dec 13, 2024
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
26 changes: 26 additions & 0 deletions public/icons/EditIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SVGProps } from 'react';

interface EditIconProps extends SVGProps<SVGSVGElement> {
width?: number;
height?: number;
}

function EditIcon({ width = 7, height = 9, ...props }: EditIconProps) {
return (
<svg
width={width}
height={height}
viewBox="0 0 7 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M3.12284 0.602564C3.30417 0.199663 3.77368 0.0288589 4.17151 0.221063L5.71719 0.967829C6.11502 1.16003 6.29053 1.64246 6.10919 2.04536L3.37194 8.12719C3.28474 8.32093 3.12502 8.46995 2.92801 8.54138L1.80364 8.94904C1.39456 9.09737 0.939295 8.87741 0.786006 8.4574L0.364689 7.30297C0.290865 7.10069 0.298382 6.87813 0.385582 6.68439L3.12284 0.602564Z"
fill="currentColor"
/>
</svg>
);
}

export default EditIcon;
5 changes: 4 additions & 1 deletion src/components/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import React from 'react';
import { twMerge } from 'tailwind-merge';
import { COLOR_SCHEMES, SIZE } from '@/constants';

interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
export interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
text: string;
size: 'large' | 'medium' | 'small' | 'modal';
fillType: 'solid' | 'outline' | 'lightSolid';
themeColor: keyof typeof COLOR_SCHEMES;
lightColor?: keyof typeof COLOR_SCHEMES;
isSubmitting?: boolean;
className?: string;
}

export default function Button({
Expand All @@ -18,6 +19,7 @@ export default function Button({
themeColor = 'green-normal-01',
lightColor,
isSubmitting,
className,
...buttonProps
}: ButtonProps) {
const { disabled } = buttonProps;
Expand Down Expand Up @@ -54,6 +56,7 @@ export default function Button({
baseClasses,
variantClasses,
isButtonDisabled && 'cursor-not-allowed',
className,
);

return (
Expand Down
48 changes: 48 additions & 0 deletions src/components/modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from '@storybook/react';
import Modal from './Modal';

const meta: Meta<typeof Modal> = {
title: 'Components/Modal',
component: Modal,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
isOpen: {
control: 'boolean',
description: '모달의 표시 여부를 제어합니다',
},
title: {
control: 'text',
description: '모달의 제목',
},
onClose: { action: '닫기 클릭' },
onConfirm: { action: '확인 클릭' },
},
};

export default meta;
type Story = StoryObj<typeof Modal>;

export const Default: Story = {
args: {
isOpen: true,
title: '모달 제목',
cancelText: '취소',
confirmText: '확인',
children: '모달에 넣을 children',
onClose: () => console.log('닫기 클릭'),
onConfirm: () => console.log('확인 클릭'),
cancelButtonProps: {
themeColor: 'gray-dark-01',
lightColor: 'gray-normal-01',
fillType: 'lightSolid',
},
confirmButtonProps: {
themeColor: 'green-normal-01',
lightColor: 'green-light-03',
fillType: 'lightSolid',
},
},
};
56 changes: 56 additions & 0 deletions src/components/modal/Modal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import '@testing-library/jest-dom';
import { render, screen, fireEvent } from '@testing-library/react';
import Modal from './Modal';

describe('Modal 컴포넌트', () => {
const defaultProps = {
isOpen: true,
onClose: jest.fn(),
title: '테스트 모달',
onConfirm: jest.fn(),
cancelText: '취소',
confirmText: '확인',
children: <div>모달 내용</div>,
};

beforeEach(() => {
jest.clearAllMocks();
});

it('isOpen이 true일 때 모달이 렌더링되어야 함', () => {
render(<Modal {...defaultProps} />);

expect(screen.getByText('테스트 모달')).toBeInTheDocument();
expect(screen.getByText('모달 내용')).toBeInTheDocument();
expect(screen.getByText('취소')).toBeInTheDocument();
expect(screen.getByText('확인')).toBeInTheDocument();
});

it('isOpen이 false일 때 모달이 렌더링되지 않아야 함', () => {
render(<Modal {...defaultProps} isOpen={false} />);

expect(screen.queryByText('테스트 모달')).not.toBeInTheDocument();
});

it('닫기 버튼 클릭 시 onClose가 호출되어야 함', () => {
render(<Modal {...defaultProps} />);

const closeButton = screen.getByLabelText('닫기');
fireEvent.click(closeButton);
expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
});

it('취소 버튼 클릭 시 onClose가 호출되어야 함', () => {
render(<Modal {...defaultProps} />);

fireEvent.click(screen.getByText('취소'));
expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
});

it('확인 버튼 클릭 시 onConfirm이 호출되어야 함', () => {
render(<Modal {...defaultProps} />);

fireEvent.click(screen.getByText('확인'));
expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1);
});
});
73 changes: 73 additions & 0 deletions src/components/modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import Button, { ButtonProps } from '../button/Button';
import { IcClose } from '../../../public/icons';

interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
title: string;
onConfirm: () => void;
cancelText: string;
confirmText: string;
Comment on lines +11 to +12
Copy link
Contributor

Choose a reason for hiding this comment

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

p5: 이건 추후에 확인, 취소 등으로 디폴트값을 설정하고 앞으로 모달작업할 때 의미가 통할만한 부분은 취소, 확인으로 고정하는식으로 가도 좋을 것 같네요. 저번에 멘토님과 멘토링하면서 느꼈던건데 디자인도 중요하지만 저희가 어느정도 가볍게 갈 수 있는 부분은 계속 전달 드리는게 좋을 것 같다는 생각이 들었습니다!

cancelButtonProps?: Partial<ButtonProps>;
confirmButtonProps?: Partial<ButtonProps>;
}

function Modal({
isOpen,
onClose,
children,
title,
onConfirm,
cancelText,
confirmText,
cancelButtonProps,
confirmButtonProps,
}: ModalProps) {
if (!isOpen) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={onClose} />

<div className="relative z-50 mx-4 max-h-[80vh] min-h-[200px] w-full min-w-[336px] max-w-[520px] overflow-y-auto rounded-lg bg-white p-6">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold">{title}</h2>
<button
onClick={onClose}
className="cursor-pointer hover:opacity-70"
aria-label="닫기"
>
<IcClose />
</button>
</div>

<div className="mb-6">{children}</div>

<div className="flex justify-between gap-2">
<Button
text={cancelText}
size="modal"
fillType="lightSolid"
themeColor="gray-normal-03"
onClick={onClose}
className="w-full"
{...cancelButtonProps}
/>
<Button
text={confirmText}
size="modal"
fillType="lightSolid"
themeColor="green-light-03"
onClick={onConfirm}
className="w-full"
{...confirmButtonProps}
/>
</div>
</div>
</div>
);
}

export default Modal;
1 change: 1 addition & 0 deletions src/constants/avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const AVATAR_SIZE = {
sm: 'h-[29px] w-[29px]',
md: 'h-[38px] w-[38px]',
lg: 'h-[56px] w-[56px]',
xl: 'h-[74px] w-[71px]',
} as const;

export type AvatarSize = keyof typeof AVATAR_SIZE;
118 changes: 118 additions & 0 deletions src/features/profile/components/ProfileEditModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { useState } from 'react';
import { useAuthStore } from '@/store/authStore';
import Avatar from '@/components/avatar/Avatar';
import EditIcon from '../../../../public/icons/EditIcon';
import Modal from '@/components/modal/Modal';

interface ProfileData {
name: string;
companyName: string;
image?: string | null;
}

interface ProfileEditModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (updatedData: ProfileData) => void;
profileData: ProfileData;
}

function ProfileEditContent({
formData,
handleChange,
}: {
formData: ProfileData;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
return (
<div className="w-full">
<div className="flex flex-col items-center gap-4">
<div className="relative">
<div className="rounded-full border-2 border-gray-normal-01">
<Avatar src={formData.image || ''} alt="프로필 이미지" size="xl" />
</div>
<button

Choose a reason for hiding this comment

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

input으로 이미지를 어떻게 추가할까요?

  <input type="file" id="imageInput" accept="image/*" />

Choose a reason for hiding this comment

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

인풋에 ref를 걸어서 ref current를 참고하면 버튼을 클릭해서 업로드를 실행시킬 수 있을 거에요.

Choose a reason for hiding this comment

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

  1. 이벤트 타겟의 파일을 확인하고
  2. 유효성 검증을 실행하고 (용량, 확장자, 여러가지)
  3. 만약 이미지 파일의 크기를 줄여야 한다면, await문으로 라이브러리 컴포넌트를 호출
  4. 최종적으로 조정된 이미지를 api요청으로 업로드
  5. 마지막으로 response를 객체를 setState해주시면 됩니다.

className="absolute bottom-0 right-0 translate-x-1/3 rounded-full border border-gray-normal-01 bg-white p-2"
onClick={() => {
/* 이미지 업로드 로직 추가 필요 */
console.log('이미지 업로드 로직 추가 필요');
}}
>
<EditIcon width={14} height={18} className="text-gray-dark-01" />
</button>
</div>

<div className="w-full space-y-4">
<div className="flex flex-col gap-2">
<span className="font-bold">닉네임</span>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full rounded-lg bg-gray-light-02 p-2 font-medium"
/>
</div>
<div className="flex flex-col gap-2">
<span className="font-bold">한 줄 소개</span>
<input
type="text"
name="companyName"
value={formData.companyName}
onChange={handleChange}
className="w-full rounded-lg bg-gray-light-02 p-2 font-medium"
/>
</div>
</div>
</div>
</div>
);
}

function ProfileEditModal({
isOpen,
onClose,
onConfirm,
profileData,
}: ProfileEditModalProps) {
const { user } = useAuthStore();

Choose a reason for hiding this comment

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

페이지를 새로고침 후에, 어떻게 유저 정보가 계속 남는지 정리하기!
persist, 로컬 스토리지.

const [formData, setFormData] = useState<ProfileData>({
name: profileData.name || user?.name || '',
companyName: profileData.companyName || user?.companyName || '',
image: profileData.image || user?.image || '/images/profile.png',
});

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};

const handleConfirm = () => {
onConfirm(formData);
};

return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="프로필 수정하기"
onConfirm={handleConfirm}
cancelText="취소하기"
confirmText="수정하기"
cancelButtonProps={{
themeColor: 'gray-dark-01',
lightColor: 'gray-normal-01',
fillType: 'lightSolid',
}}
confirmButtonProps={{
themeColor: 'green-normal-01',
lightColor: 'green-light-03',
fillType: 'lightSolid',
}}
>
<ProfileEditContent formData={formData} handleChange={handleChange} />
</Modal>
);
}

export default ProfileEditModal;
Loading
Loading