Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions src/components/common/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface DropdownProps<T extends string> {
setSelect: Dispatch<SetStateAction<T>>; // Dispatch<SetStateAction<T>>는 set함수 타입
placeholder?: string;
variant: 'form' | 'filter';
id?: string;
}

export default function Dropdown<T extends string>({
Expand All @@ -21,6 +22,7 @@ export default function Dropdown<T extends string>({
setSelect,
placeholder = '선택',
variant,
id,
}: DropdownProps<T>) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -54,6 +56,7 @@ export default function Dropdown<T extends string>({
return (
<div ref={dropdownRef} className="relative">
<button
id={id}
type="button"
onClick={toggleDropdown}
className={`flex items-center justify-between rounded-md ${variant === 'form' ? 'h-58 w-full border border-gray-30 bg-white px-20 py-16 text-body1 font-regular' : 'h-30 w-105 justify-center gap-6 rounded-[5px] bg-gray-10 px-12 text-body2 font-bold'}`}
Expand Down
7 changes: 6 additions & 1 deletion src/pages/profile/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { Link } from 'react-router-dom';
export default function Profile() {
return <div>내 프로필 상세 (알바님)</div>;
return (
<>
<Link to="/profile/edit">등록하기</Link>
</>
);
}
205 changes: 204 additions & 1 deletion src/pages/profile/ProfileForm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,206 @@
import { useState, useEffect, useCallback, useContext } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { AuthContext } from '@/context/AuthContext';
import { getUser, putUser, type SeoulDistrict } from '@/api/userApi';
import Dropdown from '@/components/common/Dropdown';
import Input from '@/components/common/Input';
import Button from '@/components/common/Button';
import Modal from '@/components/common/Modal';
import close from '@/assets/icons/close.svg';
import { ADDRESS_OPTIONS } from '@/constants/dropdownOptions';

export default function ProfileForm() {
return <div>내 프로필 등록/편집 (알바님)</div>;
const navigate = useNavigate();
const { isLoggedIn } = useContext(AuthContext);
// 사용자 입력값 상태 (이름, 전화번호, 소개글)
const [profileInfo, setProfileInfo] = useState({
name: '',
phone: '',
bio: '',
});

// dropdown 컴포넌트에 set함수를 전달하기 위해 address는 따로 분리
const [selectedAddress, setSelectedAddress] = useState<SeoulDistrict | ''>(
'',
);

const [modal, setModal] = useState({
isOpen: false,
message: '',
});

useEffect(() => {
if (isLoggedIn) {
const userId = localStorage.getItem('userId');

if (!userId) {
setModal({
isOpen: true,
message: '사용자 정보를 가져올 수 없습니다. 다시 로그인해주세요.',
});
return;
}

const fetchUserInfo = async () => {
try {
const userInfo = await getUser(userId);
setProfileInfo({
name: userInfo.item.name ?? '',
phone: userInfo.item.phone ?? '',
bio: userInfo.item.bio ?? '',
});
setSelectedAddress((userInfo.item.address as SeoulDistrict) ?? '');
} catch (error) {
setModal({
isOpen: true,
message: (error as Error).message,
});
}
};
fetchUserInfo();
} else {
setModal({
isOpen: true,
message: '로그인 먼저 해주세요.',
});
}
}, [isLoggedIn]);

function handleChange(
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) {
const { name, value } = e.target;
const sanitized = name === 'phone' ? value.replace(/[^0-9]/g, '') : value; // phone은 숫자만 입력 가능하도록 설정

setProfileInfo((prev) => ({
...prev,
[name]: sanitized,
}));
}

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const { name, phone, bio } = profileInfo;
const userId = localStorage.getItem('userId');
// 로그인이 안된 상태에 대한 처리
if (!isLoggedIn || !userId) {
setModal({
isOpen: true,
message: '로그인 먼저 해주세요.',
});
return;
}

// 이름이 입력되지 않은 경우
if (!name.trim()) {
setModal({
isOpen: true,
message: '이름을 입력해주세요.',
});
return;
}

// 지역이 선택되지 않은 경우
if (!selectedAddress) {
setModal({
isOpen: true,
message: '선호 지역을 선택해주세요',
});
return;
}

try {
await putUser(userId, {
name,
phone,
address: selectedAddress,
bio,
});
setModal({
isOpen: true,
message: '등록이 완료되었습니다.',
});
} catch (error) {
setModal({
isOpen: true,
message: (error as Error).message,
});
}
}

const handleModalConfirm = useCallback(() => {
if (modal.message === '등록이 완료되었습니다.') {
setModal({ isOpen: false, message: '' });
navigate('/profile');
} else if (modal.message.includes('로그인')) {
setModal({ isOpen: false, message: '' });
navigate('/login');
} else {
setModal({ isOpen: false, message: '' });
}
}, [modal.message, navigate]);

return (
<div className="min-h-screen bg-gray-5">
<form
className="mx-12 flex flex-col gap-24 pt-40 pb-80 md:mx-32 md:gap-32 md:py-60 lg:mx-auto lg:w-964"
onSubmit={handleSubmit}
>
<div className="flex items-center justify-between">
<h1 className="text-h3 font-bold md:text-h1">내 프로필</h1>
<Link to="/profile">
<img src={close} alt="닫기" className="md:size-32" />
</Link>
</div>
<div className="flex flex-col gap-20 md:gap-24">
<div className="grid grid-cols-1 gap-20 md:grid-cols-2 md:gap-y-24 lg:grid-cols-3">
<Input
label="이름*"
name="name"
value={profileInfo.name}
onChange={handleChange}
/>
<Input
label="연락처*"
type="tel"
name="phone"
maxLength={11}
value={profileInfo.phone}
onChange={handleChange}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

💬 figma를 보니 전화번호에 -도 들어가는데 가능하다면 구현하면 좋을 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

구현이 다 끝나고 시간이 있다면 시도해보겠습니다~!

<div className="flex flex-col gap-8 text-body1/26 font-regular">
<label htmlFor="region">선호 지역*</label>
<Dropdown
id="region"
variant="form"
options={ADDRESS_OPTIONS}
selected={selectedAddress}
setSelect={setSelectedAddress}
/>
</div>
</div>

<div className="flex flex-col gap-8 text-body1/26 font-regular">
<label htmlFor="bio">소개</label>
<textarea
name="bio"
id="bio"
className="h-153 resize-none rounded-[5px] border border-gray-30 bg-white px-20 py-16 placeholder-gray-40"
placeholder="입력"
value={profileInfo.bio}
onChange={handleChange}
></textarea>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

💬 textarea가 쓰이는 데가 있어서 다른 공통 component로 분리해도 좋을 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이미 광민님도 이렇게 진행을 한걸로 알고있어서 추후 리팩토링때 분리 고려 해보겠습니다~!

</div>
<Button type="submit" className="md:mx-auto md:w-312">
등록하기
</Button>
</form>
{modal.isOpen && (
<Modal onClose={handleModalConfirm} onButtonClick={handleModalConfirm}>
{modal.message}
</Modal>
)}
</div>
);
}