Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
748b9ce
feat: react hook form + zod 설정
Mimiminz Aug 12, 2025
a1c6893
refactor: profile-form 스키마 추가
Mimiminz Aug 12, 2025
e0ebd29
refactor: form 스키마 분리
Mimiminz Aug 12, 2025
3bf79a7
refactor: profile info 스키마 분리
Mimiminz Aug 12, 2025
afc9ef6
refactor: profile info 모달 수정
Mimiminz Aug 12, 2025
ce1a661
refactor: form field 리팩토링
Mimiminz Aug 12, 2025
c51493a
fix: event -> value 형식 변경
Mimiminz Aug 12, 2025
6938ee7
refactor: single dropdown value 기반 변경
Mimiminz Aug 13, 2025
077821e
fix: description, errorMsg 조정
Mimiminz Aug 13, 2025
7d248ca
refactor: multi dropdown
Mimiminz Aug 13, 2025
7c52987
feat: 스터디 참여 목록 타입 정의
Mimiminz Aug 19, 2025
5b19852
feat: 스터디 참여 목록 api 추가
Mimiminz Aug 19, 2025
845e74b
fix: 간단 자기소개 추가
Mimiminz Aug 19, 2025
074c827
style: 버튼 크기별 라운드 변경
Mimiminz Aug 19, 2025
be1ee08
feat: 신청 리스트 유저 카드
Mimiminz Aug 19, 2025
904a825
style: 네임 변경
Mimiminz Aug 19, 2025
06f0613
feat: 무한 스크롤 query 추가
Mimiminz Aug 20, 2025
ac40b4f
style: 백엔드와 네이밍 통일 (participation -> reservation)
Mimiminz Aug 20, 2025
9331b00
style: 자기소개 없는 경우 스타일 변경
Mimiminz Aug 20, 2025
9d4277a
refactor: entity로 변경
Mimiminz Aug 20, 2025
97cdf8b
refactor: 색상 변경 가능하게 svgr로 변경
Mimiminz Aug 20, 2025
e839c1d
feat: 신청 목록 추가 및 api 연결
Mimiminz Aug 20, 2025
80da665
style: 네이밍 변경
Mimiminz Aug 20, 2025
7e64701
refactor: rhf controller 분리
Mimiminz Aug 22, 2025
f7cb212
style: round 변경
Mimiminz Aug 22, 2025
b5d708e
refactor: feature -> entity 이동
Mimiminz Aug 22, 2025
3a043f7
refactor: profile info의 form field 및 UI 수정
Mimiminz Aug 22, 2025
597b35f
fix: 기본 이미지 렌더 오류 수정
Mimiminz Aug 22, 2025
969344f
fix: 기본 이미지로 변경값 수정
Mimiminz Aug 22, 2025
f979d6e
refactor: 토글 그룹 분리
Mimiminz Aug 23, 2025
074af24
style: 토글 버튼 사이즈 변경
Mimiminz Aug 23, 2025
de4bde2
fix: 프로필 카드 이름 변경
Mimiminz Aug 23, 2025
dac6f53
feat: 스터디 신청 스키마 추가
Mimiminz Aug 23, 2025
952a9bc
refactor: 스터디 신청 모달 변경
Mimiminz Aug 23, 2025
33e3985
refactor: 프로필 수정 변경
Mimiminz Aug 23, 2025
97e6c5b
feat: 스터디 신청 리스트 추가
Mimiminz Aug 23, 2025
d6db644
fix: field-control 사용
Mimiminz Aug 23, 2025
7e82581
refactor: 면접 관련 스키마 분리 및 rhf 적용
Mimiminz Aug 23, 2025
2f38734
feat: 유효 URL 확인용 공통 스키마 유틸 분리
Mimiminz Aug 23, 2025
f3dda4a
fix: undefined 확인 추가
Mimiminz Aug 23, 2025
be287e5
fix: 네이밍 변경
Mimiminz Aug 23, 2025
b5a36df
fix: 스키마 오류 수정
Mimiminz Aug 23, 2025
4e5e13f
feat: 스터디 현재 상태에 따른 UI 적용
Mimiminz Aug 23, 2025
b03ca1b
style: warning 제거
Mimiminz Aug 23, 2025
eeeec22
fix: 컨벤션에 맞게 네이밍 변경
Mimiminz Aug 24, 2025
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
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
Copy link
Contributor

Choose a reason for hiding this comment

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

저도 yarn으로 설치했을 때, 이 파일이 생기더라고요. 흠 왜생기는지는 저도 잘 모르겠네요 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

헉 이 파일이 추가됐군요. 혹시 모를 부분이라 추가하지 않으려 했는데 추가되었나봅니다..
저도 이 파일에 대해서 알아봤는데, 그냥 프로젝트에서 node-modules를 사용하는 방식이라고 명시해주는 거라고 하더라구요.
원래도 저희는 node-modules를 사용하고는 있었지만, 따로 명시되어 있지 않아 node-module인지, pnp인지 방식을 확실히 알 수 없다고 합니다.
그래서 저 파일을 이용해 이 프로젝트는 node-modules을 사용하는 파일이다! 라고 명시해주는 거라고 하더라구요.

문제가 되는 파일인 것 같지 않아 그냥 제 로컬에만 놔두려했는데...ㅜㅜ 커밋을 수정하는 과정에서 추가되었나봅니다.
수아님도 괜찮으시다면 이 파일은 그냥 추가해둘까 합니닷.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"api:logs": "docker-compose -f ../study-platform-mvp/docker-compose.yml logs -f mvp-app"
},
"dependencies": {
"@hookform/resolvers": "^5.2.1",
"@next/third-parties": "^15.3.3",
"@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-dialog": "^1.1.10",
Expand All @@ -37,10 +38,12 @@
"react": "^19.0.0",
"react-day-picker": "9.4.3",
"react-dom": "^19.0.0",
"react-hook-form": "^7.62.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.0.6",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.2.5",
"zod": "^4.0.17",
"zustand": "^5.0.3"
},
"devDependencies": {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/features/auth/ui/login-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export default function LoginModal({
? '616205933420-b45d510q23togkaqo069j8igmsjhp9v0.apps.googleusercontent.com'
: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;

const NAVER_LOGIN_URL = '';
const KAKAO_LOGIN_URL = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_CLIENT_ID}&redirect_uri=${API_BASE_URL}/api/v1/auth/kakao/redirect-uri&response_type=code&state=${state}`;
const GOOGLE_LOGIN_URL = `https://accounts.google.com/o/oauth2/v2/auth?scope=openid%20profile&access_type=offline&prompt=consent&include_granted_scopes=true&response_type=code&redirect_uri=${API_BASE_URL}/api/v1/auth/google/redirect-uri&client_id=${GOOGLE_CLIENT_ID}&state=${state}`;

Expand Down
12 changes: 5 additions & 7 deletions src/features/auth/ui/sign-up-image-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { PencilIcon } from 'lucide-react';
import Image from 'next/image';
import UserAvatar from '@/shared/ui/avatar';

export default function SignupImageSelector({
image,
setImage,
fileInputRef,
handleImageChange,
}: {
image: string;
setImage: (image: string) => void;
image?: string;
setImage: (image?: string) => void;
fileInputRef: React.RefObject<HTMLInputElement>;
handleImageChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}) {
const setDefaultImage = () => setImage('/profile-default.svg');
const setDefaultImage = () => setImage(undefined);
const openFileFolder = () => fileInputRef.current?.click();

return (
<div className="relative">
<div className="relative h-[112px] w-[112px] overflow-hidden rounded-full">
<Image src={image} alt="프로필" fill className="object-cover" />
</div>
<UserAvatar image={image} size={112} />
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button
Expand Down
106 changes: 106 additions & 0 deletions src/features/my-page/model/profile-form.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { FieldNamesMarkedBoolean } from 'react-hook-form';
import { z } from 'zod';
import type { MemberProfile } from '@/entities/user/api/types';
import { UrlSchema } from '@/shared/util/zod-schema';
import { UpdateUserProfileRequest } from '../api/types';

const nameRegex = /^[가-힣a-zA-Z]{2,10}$/;
const telRegex = /^\d{2,3}-\d{3,4}-\d{4}$/;
const birthRegex = /^\d{4}\.(0[1-9]|1[0-2])\.(0[1-9]|[12]\d|3[01])$/;

export const ProfileFormSchema = z.object({
name: z
.string()
.trim()
.min(1, '이름은 필수입니다.')
.regex(nameRegex, '이름은 2~10자의 한글 또는 영문만 허용됩니다.'),

tel: z
.string()
.trim()
.min(1, '연락처는 필수입니다.')
.regex(telRegex, '연락처는 숫자와 하이픈(-) 형식으로 입력해주세요.'),

birthDate: z
.string()
.trim()
.optional()
.transform((v) => (v === '' || v === undefined ? undefined : v))
.refine((v) => v === undefined || birthRegex.test(v), {
message: '잘못된 형식입니다.',
})
.refine(
(v) => {
if (!v) return true;
const d = new Date(v.replace(/\./g, '-'));
const year = d.getFullYear();
const now = new Date().getFullYear();

return !Number.isNaN(d.getTime()) && year >= 1900 && year <= now;
},
{ message: '잘못된 형식입니다.' },
)
.transform((v) => (v ? v.replace(/\./g, '-') : undefined)),

githubLink: UrlSchema,
blogOrSnsLink: UrlSchema,

simpleIntroduction: z
.string()
.trim()
.max(200, '최대 200자까지 입력할 수 있어요.')
.optional()
.transform((v) => (v === '' || v === undefined ? undefined : v)),

mbti: z
.string()
.optional()
.transform((v) => (v === '' || v === undefined ? undefined : v)),

interests: z
.array(z.string())
.optional()
.transform((arr) => (!arr || arr.length === 0 ? undefined : arr)),
});

export type ProfileFormInput = z.input<typeof ProfileFormSchema>;
export type ProfileFormValues = z.output<typeof ProfileFormSchema>;

export function buildProfileDefaultValues(
member: MemberProfile,
): ProfileFormInput {
return {
name: member.memberName ?? '',
tel: member.tel ?? '',
// 화면에서는 점(.)으로 보여주기, 스키마 제출 시 하이픈(-) 변환
birthDate: member.birthDate ? member.birthDate.replace(/-/g, '.') : '',
githubLink: member.githubLink?.url ?? '',
blogOrSnsLink: member.blogOrSnsLink?.url ?? '',
mbti: (member.mbti as string) ?? '',
simpleIntroduction: member.simpleIntroduction ?? '',
interests: member.interests?.map((i) => i.name) ?? [],
};
}

// 서버 전송 payload
export function toUpdateProfilePayload(
v: ProfileFormValues,
extra?: Partial<Pick<UpdateUserProfileRequest, 'profileImageExtension'>>,
): UpdateUserProfileRequest {
const payload: UpdateUserProfileRequest = {
name: v.name,
tel: v.tel,
birthDate: v.birthDate,
githubLink: v.githubLink,
blogOrSnsLink: v.blogOrSnsLink,
simpleIntroduction: v.simpleIntroduction,
mbti: v.mbti,
interests: v.interests,
};

if (extra?.profileImageExtension) {
payload.profileImageExtension = extra.profileImageExtension;
}

return payload;
}
60 changes: 60 additions & 0 deletions src/features/my-page/model/profile-info-form.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { z } from 'zod';
import type { MemberInfo } from '@/entities/user/api/types';
import type { UpdateUserProfileInfoRequest } from '../api/types';

export const ProfileInfoFormSchema = z.object({
selfIntroduction: z
.string()
.trim()
.max(500, '최대 500자까지 입력 가능합니다.'),

studyPlan: z
.string()
.trim()
.min(1, '공부 계획을 입력해 주세요.')
.max(500, '최대 500자까지 입력 가능합니다.'),

preferredStudySubjectId: z.string().optional(),

availableStudyTimeIds: z
.array(z.string())
.min(1, '가능 시간대를 1개 이상 선택해 주세요.'),

techStackIds: z
.array(z.string())
.min(1, '기술 스택을 1개 이상 선택해 주세요.'),
});

export type ProfileInfoFormValues = z.infer<typeof ProfileInfoFormSchema>;

export function buildProfileInfoDefaultValues(
member: MemberInfo,
): ProfileInfoFormValues {
return {
selfIntroduction: member.selfIntroduction ?? '',
studyPlan: member.studyPlan ?? '',
preferredStudySubjectId: member.preferredStudySubject
? String(member.preferredStudySubject.studySubjectId)
: undefined,
availableStudyTimeIds: (member.availableStudyTimes ?? [])
.map((t) => t?.id)
.filter((x): x is number => typeof x === 'number')
.map(String),
techStackIds: (member.techStacks ?? [])
.map((t) => t?.techStackId)
.filter((x): x is number => typeof x === 'number')
.map(String),
};
}

export function toUpdateUserProfileInfoRequest(
v: ProfileInfoFormValues,
): UpdateUserProfileInfoRequest {
return {
selfIntroduction: v.selfIntroduction,
studyPlan: v.studyPlan,
preferredStudySubjectId: v.preferredStudySubjectId!,
availableStudyTimeIds: v.availableStudyTimeIds.map(Number),
techStackIds: v.techStackIds.map(Number),
};
}
Comment on lines +30 to +60
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
Contributor Author

Choose a reason for hiding this comment

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

넵 컨벤션이라기엔 좀 민망하지만
이왕 form 변경하는 김에 동일한 형식으로 작성했습니다.

Loading