Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions src/components/common/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import Down from '@/assets/icons/dropdown.svg';

interface DropdownProps<T extends string> {
options: readonly T[];
selected: T;
setSelect: Dispatch<SetStateAction<T>>; // Dispatch<SetStateAction<T>>는 set함수 타입
selected: T | null;
setSelect: Dispatch<SetStateAction<T | null>>; // 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>
</>
);
}
206 changes: 205 additions & 1 deletion src/pages/profile/ProfileForm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,207 @@
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 | null>(
null,
);

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-[calc(100vh-102px)] bg-gray-5 md:min-h-[calc(100vh-70px)]">
Copy link
Contributor

Choose a reason for hiding this comment

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

별로 좋은 방식은 아닌 것 같지만... 일단 확인했습니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

혹시 어떤 방식이 또 있을까요?? 전 저거밖에 생각이 안나서 저렇게 작성했습니다

Copy link
Contributor

Choose a reason for hiding this comment

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

저도 마땅히 떠오르진 않네요? 초기에 말한 것처럼 Nav 컴포넌트를 page 컴포넌트 내에 넣고 h-screen으로 활용하는 게 제일 좋지 않나 생각하지만 일단 밖으로 빼서 더 좋은 방법이 생각 나지는 않네요

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="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">
<div className="flex items-center justify-between">
<h1 className="text-h3/24 font-bold md:text-h1/34">내 프로필</h1>
<Link to="/profile">
<img src={close} alt="닫기" className="md:size-32" />
</Link>
</div>
<form
className="flex flex-col gap-24 md:gap-32"
onSubmit={handleSubmit}
>
<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}
/>
<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>
</div>
<Button type="submit" className="md:mx-auto md:w-312">
등록하기
</Button>
</form>
</div>
{modal.isOpen && (
<Modal onClose={handleModalConfirm} onButtonClick={handleModalConfirm}>
{modal.message}
</Modal>
)}
</div>
);
}