diff --git a/app/(my)/layout.tsx b/app/(my)/layout.tsx
index 76a6fae5..c4c054fd 100644
--- a/app/(my)/layout.tsx
+++ b/app/(my)/layout.tsx
@@ -14,7 +14,9 @@ export default function MyLayout({
return (
-
{children}
+
+ {children}
+
);
}
diff --git a/app/global.css b/app/global.css
index bcbecc55..74119091 100644
--- a/app/global.css
+++ b/app/global.css
@@ -185,7 +185,7 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E
--color-background-alternative: var(--color-gray-50);
--color-background-default: var(--color-gray-0);
- --color-background-dimmer: rgba(24, 29, 39, 80);
+ --color-background-dimmer: rgba(24, 29, 39, 0.8);
--color-background-disabled: var(--color-gray-100);
--color-background-brand-subtle: var(--color-rose-300);
diff --git a/public/profile-default.jpg b/public/profile-default.jpg
new file mode 100644
index 00000000..46ce5400
Binary files /dev/null and b/public/profile-default.jpg differ
diff --git a/src/features/auth/api/auth.ts b/src/features/auth/api/auth.ts
index a055640c..26828943 100644
--- a/src/features/auth/api/auth.ts
+++ b/src/features/auth/api/auth.ts
@@ -19,13 +19,10 @@ export async function uploadProfileImage(
filename: string,
file: FormData,
) {
- console.log('uploadProfileImage 요청직전', file);
- // Bug : 여기서 에러발생중
const res = await axiosInstanceForMultipart.put(
`/files/members/${memberId}/profile/image/${filename}`,
file,
);
- console.log('auth.ts - uploadProfileImage 응답', res);
return res.data;
}
diff --git a/src/features/auth/ui/sign-up-image-selector.tsx b/src/features/auth/ui/sign-up-image-selector.tsx
index 4e7fe757..8a8f9ce0 100644
--- a/src/features/auth/ui/sign-up-image-selector.tsx
+++ b/src/features/auth/ui/sign-up-image-selector.tsx
@@ -14,13 +14,13 @@ export default function SignupImageSelector({
handleImageChange: (event: React.ChangeEvent) => void;
}) {
const setDefaultImage = () => setImage('/profile-default.svg');
- const openFileDialog = () => fileInputRef.current?.click();
+ const openFileFolder = () => fileInputRef.current?.click();
return (
diff --git a/src/features/my-page/consts/my-page-const.ts b/src/features/my-page/consts/my-page-const.ts
index 15b20a72..5f909266 100644
--- a/src/features/my-page/consts/my-page-const.ts
+++ b/src/features/my-page/consts/my-page-const.ts
@@ -29,3 +29,5 @@ export const MBTI_OPTIONS = [
{ label: 'ENFJ', value: 'ENFJ' },
{ label: 'ENTJ', value: 'ENTJ' },
];
+
+export const DEFAULT_PROFILE_IMAGE_URL = '/profile-default.svg';
diff --git a/src/features/my-page/ui/profile-edit-modal.tsx b/src/features/my-page/ui/profile-edit-modal.tsx
index 02cdb8a0..9c26e4a8 100644
--- a/src/features/my-page/ui/profile-edit-modal.tsx
+++ b/src/features/my-page/ui/profile-edit-modal.tsx
@@ -10,57 +10,82 @@ import Button from '@/shared/ui/button';
import { Modal } from '@/shared/ui/modal';
import { FormField } from '../../../shared/ui/form/form-field';
import { UpdateUserProfileRequest } from '../api/types';
-import { updateUserProfile } from '../api/update-user-profile';
-import { DEFAULT_OPTIONS, MBTI_OPTIONS } from '../consts/my-page-const';
+import {
+ DEFAULT_OPTIONS,
+ DEFAULT_PROFILE_IMAGE_URL,
+ MBTI_OPTIONS,
+} from '../consts/my-page-const';
+import { useUpdateUserProfileMutation } from '../model/use-update-user-profile-mutation';
interface Props {
- onSubmit: (formData: UpdateUserProfileRequest) => void;
memberProfile: MemberProfile;
memberId: number;
}
export type MbtiValue = (typeof MBTI_OPTIONS)[number]['value'];
-export default function ProfileEditModal({
- onSubmit,
- memberProfile,
- memberId,
-}: Props) {
- const [name, setName] = useState(
- memberProfile.memberName ?? '',
- );
- const [tel, setTel] = useState(
- memberProfile.tel ?? '',
- );
- const [githubLink, setGithubLink] = useState<
- UpdateUserProfileRequest['githubLink']
- >(memberProfile.githubLink?.url ?? '');
-
- const [blogOrSnsLink, setBlogOrSnsLink] = useState<
- UpdateUserProfileRequest['blogOrSnsLink']
- >(memberProfile.blogOrSnsLink?.url ?? '');
+export default function ProfileEditModal({ memberProfile, memberId }: Props) {
+ const [isOpen, setIsOpen] = useState(false);
- const [mbti, setMbti] = useState(
- memberProfile.mbti ?? '',
+ return (
+
+ setIsOpen(true)}
+ className="rounded-100 bg-fill-brand-default-default font-designer-16b text-text-inverse w-full px-150 py-100"
+ >
+ 내 프로필 수정
+
+
+
+
+
+
+ 내 프로필 수정
+
+
+
+
+
+ setIsOpen(false)}
+ />
+
+
+
);
+}
- const [simpleIntroduction, setSimpleIntroduction] = useState<
- UpdateUserProfileRequest['simpleIntroduction']
- >(memberProfile.simpleIntroduction ?? '');
-
- const [interests, setInterests] = useState<
- UpdateUserProfileRequest['interests']
- >(memberProfile.interests?.map((item) => item.name) ?? []);
+function ProfileEditForm({
+ memberProfile,
+ memberId,
+ onClose,
+}: Props & { onClose: () => void }) {
+ const fileInputRef = useRef(null);
+ const [profileForm, setProfileForm] = useState({
+ name: memberProfile.memberName ?? '',
+ tel: memberProfile.tel ?? '',
+ githubLink: memberProfile.githubLink?.url ?? '',
+ blogOrSnsLink: memberProfile.blogOrSnsLink?.url ?? '',
+ mbti: memberProfile.mbti ?? '',
+ simpleIntroduction: memberProfile.simpleIntroduction ?? '',
+ interests: memberProfile.interests?.map((item) => item.name) ?? [],
+ });
const [image, setImage] = useState(
memberProfile.profileImage?.resizedImages?.[0]?.resizedImageUrl ??
- '/profile-default.svg',
+ DEFAULT_PROFILE_IMAGE_URL,
);
- const [isOpen, setIsOpen] = useState(false);
- const fileInputRef = useRef(null);
- const uploadProfileImage = useUploadProfileImageMutation();
+ // 이름 유효성 검사: 2~10자, 한글 또는 영문만 허용
+ const isNameValid = /^[가-힣a-zA-Z]{2,10}$/.test(profileForm.name);
+ // 연락처 유효성 검사: "(2~3자리 지역번호)-(3~4자리 번호)-(4자리 번호)" 형식
+ const isTelValid = /^\d{2,3}-\d{3,4}-\d{4}$/.test(profileForm.tel);
+
const queryClient = useQueryClient();
+ const { mutateAsync: updateProfile } = useUpdateUserProfileMutation(memberId);
+ const { mutateAsync: uploadProfileImage } = useUploadProfileImageMutation();
const handleImageChange = (e: React.ChangeEvent) => {
if (e.target.files && e.target.files[0]) {
@@ -70,45 +95,63 @@ export default function ProfileEditModal({
};
const handleSubmit = async () => {
- if (!name || !tel) {
- alert('모든 필수 정보를 입력해주세요!');
-
- return;
- }
-
const file = fileInputRef.current?.files?.[0];
-
- const hasImageFile = !!file;
- const ext = file?.name.split('.').pop()?.toUpperCase();
- const profileImageExtension =
- ext && ['JPG', 'PNG', 'GIF', 'WEBP'].includes(ext) ? ext : undefined;
+ const profileImageExtension = file?.name.split('.').pop();
const rawFormData: UpdateUserProfileRequest = {
- name,
- tel,
- githubLink: githubLink.trim() || undefined,
- blogOrSnsLink: blogOrSnsLink.trim() || undefined,
- simpleIntroduction: simpleIntroduction.trim() || undefined,
- mbti: mbti.trim() || undefined,
- interests: interests.length > 0 ? interests : undefined,
- profileImageExtension: hasImageFile ? profileImageExtension : undefined,
+ name: profileForm.name,
+ tel: profileForm.tel,
+ githubLink: profileForm.githubLink,
+ blogOrSnsLink: profileForm.blogOrSnsLink,
+ simpleIntroduction: profileForm.simpleIntroduction.trim() || undefined,
+ mbti: profileForm.mbti || undefined,
+ interests:
+ profileForm.interests.length > 0 ? profileForm.interests : undefined,
+ profileImageExtension: profileImageExtension,
};
const formData = Object.fromEntries(
Object.entries(rawFormData).filter(([_, v]) => v !== undefined),
) as UpdateUserProfileRequest;
- const updated = await updateUserProfile(memberId, formData);
+ const updatedProfile = await updateProfile(formData);
+
+ // todo: 서버 측에서 api 수정되면, 리팩토링
+ // 기본 프로필 이미지를 선택할 경우, 기본 프로필 이미지 전송 -> 서버 측에서 나중에 리팩토링 예정
+ // public 폴더의 profile-default.jpg 파일을 fetch해서 FormData에 추가
+ // profile-default.jpg를 추가한 이유는 서버에서 프로필 이미지 확장자에 svg 파일을 고려하지 못함 -> 서버 측에서 리팩토링 예정
+ if (image === DEFAULT_PROFILE_IMAGE_URL) {
+ try {
+ const defaultProfileImage = 'profile-default.jpg';
+ const response = await fetch(defaultProfileImage);
+ const blob = await response.blob();
+ const defaultFile = new File([blob], defaultProfileImage, {
+ type: 'image/jpeg',
+ });
+ const imageFormData = new FormData();
+ imageFormData.append('file', defaultFile);
+
+ await uploadProfileImage({
+ memberId,
+ filename: defaultProfileImage,
+ file: imageFormData,
+ });
+ } catch (error) {
+ console.error('기본 프로필 이미지 업로드 실패:', error);
+ alert('기본 프로필 이미지 업로드에 실패했습니다.');
+ }
+ }
- if (fileInputRef.current?.files?.[0] && updated.profileImageUploadUrl) {
+ // 폴더에서 이미지를 선택한 경우
+ if (file && updatedProfile.profileImageUploadUrl) {
const imageFormData = new FormData();
- imageFormData.append('file', fileInputRef.current.files[0]);
+ imageFormData.append('file', file);
- const filename = updated.profileImageUploadUrl.split('/').pop();
+ const filename = updatedProfile.profileImageUploadUrl.split('/').pop();
if (!filename) return;
try {
- await uploadProfileImage.mutateAsync({
+ await uploadProfileImage({
memberId,
filename,
file: imageFormData,
@@ -118,119 +161,132 @@ export default function ProfileEditModal({
alert('이미지 업로드에 실패했습니다.');
}
}
- await queryClient.invalidateQueries({ queryKey: ['memberInfo'] });
- onSubmit(formData);
+ await queryClient.invalidateQueries({
+ queryKey: ['userProfile', memberId],
+ });
};
return (
-
- setIsOpen(true)}
- className="rounded-100 bg-fill-brand-default-default font-designer-16b text-text-inverse w-full px-150 py-100"
- >
- 내 프로필 수정
-
-
-
-
-
-
- 내 프로필 수정
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
{
+ // 공백 입력하지 못하도록 제한
+ setProfileForm({
+ ...profileForm,
+ name: value.replace(/\s/g, ''),
+ });
+ }}
+ required
+ />
+ {
+ // 숫자와 하이픈(-)만 입력 허용
+ const onlyNumberAndHyphen = value.replace(/[^\d-]/g, '');
+ setProfileForm({ ...profileForm, tel: onlyNumberAndHyphen });
+ }}
+ required
+ />
+
+ setProfileForm({
+ ...profileForm,
+ githubLink: value.replace(/\s/g, ''),
+ })
+ }
+ />
+
+ setProfileForm({ ...profileForm, mbti: value })
+ }
+ options={MBTI_OPTIONS}
+ />
+
+ setProfileForm({ ...profileForm, interests: value })
+ }
+ options={DEFAULT_OPTIONS}
+ />
+
+ setProfileForm({ ...profileForm, simpleIntroduction: value })
+ }
+ />
+
+ setProfileForm({
+ ...profileForm,
+ blogOrSnsLink: value.replace(/\s/g, ''),
+ })
+ }
+ />
+
+
+
+
+
+
+
+
+ >
);
}
diff --git a/src/features/my-page/ui/profile.tsx b/src/features/my-page/ui/profile.tsx
index d31c1fa4..811cbc3f 100644
--- a/src/features/my-page/ui/profile.tsx
+++ b/src/features/my-page/ui/profile.tsx
@@ -1,12 +1,8 @@
-'use client';
-
import Image from 'next/image';
import { MemberProfile } from '@/entities/user/api/types';
import ProfileEditModal from '@/features/my-page/ui/profile-edit-modal';
import UserAvatar from '@/shared/ui/avatar';
import Badge from '@/shared/ui/badge';
-import { UpdateUserProfileRequest } from '../api/types';
-import { useUpdateUserProfileMutation } from '../model/use-update-user-profile-mutation';
interface ProfileProps {
memberId: number;
@@ -14,12 +10,6 @@ interface ProfileProps {
}
export default function Profile({ memberId, memberProfile }: ProfileProps) {
- const { mutate } = useUpdateUserProfileMutation(memberId);
-
- const handleSubmit = (formData: UpdateUserProfileRequest) => {
- mutate(formData);
- };
-
return (
- {memberProfile.githubLink?.url ?? '깃허브 링크를 입력해주세요!'}
+ {memberProfile.githubLink?.url || '깃허브 링크를 입력해주세요!'}
- {memberProfile.blogOrSnsLink?.url ??
+ {memberProfile.blogOrSnsLink?.url ||
'블로그 링크를 입력해주세요!'}
-
+
);
diff --git a/src/shared/lib/get-login-user.ts b/src/shared/lib/get-login-user.ts
index 58110f4b..cdceb3a5 100644
--- a/src/shared/lib/get-login-user.ts
+++ b/src/shared/lib/get-login-user.ts
@@ -1,6 +1,6 @@
-import { getServerCookie, setServerCookie } from './server-cookie';
import axios from 'axios';
import { redirect } from 'next/navigation';
+import { getServerCookie, setServerCookie } from './server-cookie';
export async function getLoginUserId(): Promise {
// memberId 쿠키 우선 확인 (기존 로직)
@@ -17,7 +17,7 @@ export async function getLoginUserId(): Promise {
// 1. accessToken 쿠키 확인
let accessToken = await getServerCookie('accessToken');
if (!accessToken) return null;
-
+
// 2. accessToken으로 /auth/me 호출
try {
// (SSR 이므로 useQuery 를 쓰지 않고 직접 API 호출)
@@ -26,20 +26,19 @@ export async function getLoginUserId(): Promise {
{
headers: { Authorization: `Bearer ${accessToken}` },
withCredentials: true,
- }
+ },
);
// 3. memberId 쿠키와 /auth/me의 memberId가 다르면 null 반환
if (Number(res.data.content) !== memberId) return null;
return memberId;
} catch (error: any) {
-
// 4. 401이면 만료된 토큰이므로 토큰 갱신 시도
if (error.response?.status === 401) {
try {
const refreshRes = await axios.get(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/auth/access-token/refresh`,
- { withCredentials: true }
+ { withCredentials: true },
);
accessToken = refreshRes.data.accessToken;
// 5. SSR에서 최신화된 AccessToken을 쿠키등록
@@ -51,10 +50,10 @@ export async function getLoginUserId(): Promise {
{
headers: { Authorization: `Bearer ${accessToken}` },
withCredentials: true,
- }
+ },
);
if (Number(res2.data.memberId) !== memberId) return null;
-
+
return memberId;
} catch (refreshError) {
// 갱신 실패 → 로그인 페이지로 리다이렉트
diff --git a/src/shared/ui/form/form-field.tsx b/src/shared/ui/form/form-field.tsx
index f617a5d6..737f3edf 100644
--- a/src/shared/ui/form/form-field.tsx
+++ b/src/shared/ui/form/form-field.tsx
@@ -21,10 +21,12 @@ interface FormFieldProps {
value: T;
direction?: 'horizontal' | 'vertical';
onChange: (value: T) => void;
+ error?: boolean;
}
export function FormField({
label,
+ error = false,
description,
type,
required = false,
@@ -42,10 +44,13 @@ export function FormField({
onChange(e.target.value as T)}
/>
{description && (
-