Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f20b154
feat: initialize surface
yunko1803 May 13, 2025
9c994d3
chore: merge with develop
yunko1803 May 13, 2025
bd162a5
feat: add one section of profile update
yunko1803 May 17, 2025
fcc5db6
feat: create intro section
yunko1803 May 17, 2025
e58c9f8
feat: add link section
yunko1803 May 17, 2025
f76fa5b
chore: eslint
yunko1803 May 17, 2025
47232af
feat: complete user education form section
yunko1803 May 19, 2025
65cbf33
chore: fix convention
yunko1803 May 19, 2025
a93ef66
feat: create UI career section
yunko1803 May 20, 2025
8a720a2
feat: create UI section for tech stack
yunko1803 May 20, 2025
c2f8709
feat: create career section
yunko1803 May 23, 2025
81b2484
feat: complete tech stack sections
yunko1803 May 23, 2025
a2b833a
feat: create profile update sidebar
yunko1803 May 23, 2025
9c23bfb
chore: fix eslint
yunko1803 May 23, 2025
0dbc372
feat: test api
yunko1803 May 23, 2025
dcd48dd
feat: make it able to use current data
yunko1803 May 27, 2025
179e0eb
feat: add how to load current data on update form
yunko1803 May 28, 2025
3bc2168
fix: deployment error
yunko1803 Jun 2, 2025
d0327cb
fix: feedback
yunko1803 Jun 13, 2025
3ed169a
refactor: create common component for register related components
yunko1803 Jun 13, 2025
0d4d0a5
fix: feedback
yunko1803 Jun 13, 2025
000c9ad
fix: functionality for dropdown
yunko1803 Jun 13, 2025
87ca4db
feat: fix location of remove button
yunko1803 Jun 15, 2025
6de266c
fix: profile update progress and category
yunko1803 Jun 18, 2025
c79cd24
fix: profile update form
yunko1803 Jun 19, 2025
3db1eeb
fix: invalidateQuery
yunko1803 Jun 19, 2025
92ff395
fix: comment feedback
yunko1803 Jun 27, 2025
4f9ab9f
fix: change from router.push to window.href
yunko1803 Jul 2, 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
11 changes: 6 additions & 5 deletions src/app/onboarding/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ERROR_MESSAGES } from '@/constants/error';
import { useSignUp } from '@/service/auth/queries';
import AuthQueryOptions from '@/service/auth/queries';
import BelongingQueryOptions from '@/service/belonging/queries';
import { isEmpty } from '@/utils/array';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
Expand Down Expand Up @@ -51,7 +52,7 @@ const Onboarding = () => {
}, [affiliation]);

const { queryKey, queryFn } = BelongingQueryOptions.search(debouncedAffiliation);
const { data: belongingData = [] } = useQuery({
const { data: belongingData } = useQuery({
queryKey,
queryFn,
enabled: !!debouncedAffiliation
Expand Down Expand Up @@ -132,12 +133,12 @@ const Onboarding = () => {
setValue={handleAffiliationChange}
placeholder="학교, 회사 등 현재 소속을 입력해 주세요"
renderDropdown={() =>
isDropdownVisible && belongingData.length > 0 ? (
isDropdownVisible && belongingData && !isEmpty(belongingData.content) ? (
<Dropdown
list={belongingData}
searchCount={belongingData.length}
list={belongingData.content}
searchCount={belongingData.total}
onSelect={(suggestion) => {
setAffiliation(suggestion);
setAffiliation(suggestion.name);
closeDropdown();
}}
closeDropdown={closeDropdown}
Expand Down
11 changes: 6 additions & 5 deletions src/app/profile/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ProjectSectionSkeleton from '@/components/profile/ProjectSectionSkeleton'

interface ProfilePageProps {
params: {
memberId: string;
memberId: number;
};
}

Expand All @@ -26,7 +26,7 @@ const Profile = async ({ params }: ProfilePageProps) => {

const [userProfileQuery, userProjectsQuery] = await getDehydratedQueries(queries);
const profileDetail = userProfileQuery?.state.data as UserProfileResponse;
const { title, contents } = profileDetail.myPage.selfIntroduction;
const { title, contents } = profileDetail.myPage.selfIntroduction ?? { title: '', contents: '' };
const { techStacks, links } = profileDetail.myPage;
const showUserSkeleton = !title && !contents && isEmpty(techStacks) && isEmpty(links);
const projects = userProjectsQuery?.state.data as Array<UserProject>;
Expand Down Expand Up @@ -60,9 +60,10 @@ const Profile = async ({ params }: ProfilePageProps) => {
<ProjectPortfolioSection projects={projects} memberId={memberId} />
)}

{!isEmpty(profileDetail.myPage.careers.careerList) && (
<ProfileCareerSection careers={profileDetail.myPage.careers} />
)}
{profileDetail?.myPage?.careers?.careerList &&
!isEmpty(profileDetail.myPage.careers.careerList) && (
<ProfileCareerSection careers={profileDetail.myPage.careers} />
)}
{!isEmpty(profileDetail.myPage.educationActivities) && (
<ProfileEducationSection education={profileDetail.myPage.educationActivities} />
)}
Expand Down
21 changes: 21 additions & 0 deletions src/app/profile/update/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { isLoggedIn } from '@/service/auth/queries';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

import ProfileUpdateForm from '@/components/profile/update/ProfileUpdateForm';

const ProfileUpdatePage = async () => {
const isUser = await isLoggedIn(cookies);

if (!isUser) {
redirect('/404');
}

return (
<div className="p-10">
<ProfileUpdateForm />
</div>
);
};

export default ProfileUpdatePage;
61 changes: 48 additions & 13 deletions src/components/common/Dropdown/SDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,86 @@
'use client';

import React, { useEffect, useState } from 'react';
import { FieldValues, Path, RegisterOptions, UseFormRegister } from 'react-hook-form';
import { Nullable } from 'types/common';

import SvgIcon from '../SVGIcon';

interface SDropdownProps<T = string> {
options: T[];
onChange?: (selectedOption: Nullable<T>) => void;
interface SDropdownProps<TOption = string, TForm extends FieldValues = FieldValues> {
options: TOption[];
value?: Nullable<TOption>;
onChange?: (selectedOption: Nullable<TOption>) => void;
placeholder?: string;
className?: string;
label?: 'default' | 'bold' | 'none';
name?: Path<TForm>;
register?: UseFormRegister<TForm>;
registerOptions?: RegisterOptions<TForm>;
}

const SDropdown = <T,>({
const BORDER_STYLE_BY_LABEL = {
default: 'border border-slate-90 rounded-[1rem]',
bold: 'border border-slate-300 rounded-base',
none: 'border-0 rounded-base'
};

const SDropdown = <TOption, TForm extends FieldValues = FieldValues>({
options,
value = null,
onChange = () => {},
placeholder = 'Select Option',
className
}: SDropdownProps<T>) => {
const [selectedOption, setSelectedOption] = useState<Nullable<T>>(null);
className,
label = 'default',
register,
name,
registerOptions
}: SDropdownProps<TOption, TForm>) => {
const [selectedOption, setSelectedOption] = useState<Nullable<TOption>>(null);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);

const handleSelectOption = (option: T) => {
useEffect(() => {
setSelectedOption(value);
}, [value]);

const handleSelectOption = (option: TOption) => {
setSelectedOption(option);
setIsDropdownOpen(false);
onChange(option);
};

useEffect(() => {
onChange(selectedOption);
}, [selectedOption, onChange]);
const registration = register && name ? register(name, registerOptions) : undefined;

return (
<div className={`relative w-full ${className}`}>
{registration && (
<input
type="hidden"
value={selectedOption ? String(selectedOption) : ''}
{...registration}
/>
)}
<div
className={`w-full border border-slate-90 rounded-[1rem] flex items-center justify-between pr-3 cursor-pointer ${isDropdownOpen ? 'ring-1 ring-tree-50' : ''}`}
className={`w-full ${BORDER_STYLE_BY_LABEL[label]} flex items-center justify-between pr-3 cursor-pointer ${
isDropdownOpen ? 'ring-1 ring-tree-50' : ''
}`}
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
<span className="p-3 text-small font-md leading-5 tracking-[-0.14px] text-slate-50">
<span
className={`p-3 text-small font-md leading-5 tracking-[-0.14px] text-slate-${!selectedOption ? '50' : '10'}`}
>
{selectedOption ? String(selectedOption) : placeholder}
</span>
<div className="w-4 h-4 flex-shrink-0">
<SvgIcon
icon="arrow"
width={16}
height={16}
color="#959EB2"
className={isDropdownOpen ? 'transform scale-y-[-1]' : ''}
/>
</div>
</div>

{options.length > 0 && isDropdownOpen && (
<div className="absolute z-10 bg-white-100 border border-gray-300 rounded-lg w-full mt-2">
<div className="flex flex-col gap-1 p-2">
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/Dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useEffect, useRef } from 'react';
interface DropdownProps<T extends { name: string }> {
list: T[];
searchCount: number;
onSelect: (item: string) => void;
onSelect: (item: T) => void;
closeDropdown: () => void;
}

Expand Down Expand Up @@ -43,7 +43,7 @@ const Dropdown = <T extends { name: string }>({
<li
key={index}
className="px-4 py-2 text-[14px] text-gray-800 hover:bg-slate-95 hover:text-green-600 cursor-pointer"
onClick={() => onSelect(item.name)}
onClick={() => onSelect(item)}
>
{item.name}
</li>
Expand Down
11 changes: 9 additions & 2 deletions src/components/common/Input/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { FieldValues, UseFormRegister } from 'react-hook-form';

import SImage from '../Image';
Expand All @@ -19,6 +19,7 @@ interface InputProps<T extends FieldValues> {
limitLength?: number; // 글자 수 제한 길이
onEnterPress?: (contents: string) => void; // 엔터 키 눌렀을 때 실행할 함수 추가
onIconClick?: (contents: string) => void; // 아이콘 클릭 시 실행할 함수 추가
renderDropdown?: () => React.ReactNode;
}

/* eslint-disable @typescript-eslint/no-explicit-any */
Expand All @@ -37,7 +38,8 @@ const SInput = React.forwardRef<HTMLInputElement, InputProps<any>>(
useLimit = false,
limitLength,
onEnterPress,
onIconClick
onIconClick,
renderDropdown
},
ref
) => {
Expand All @@ -49,6 +51,10 @@ const SInput = React.forwardRef<HTMLInputElement, InputProps<any>>(

const [text, setText] = useState<string>(value || '');

useEffect(() => {
setText(value || '');
}, [value]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (useLimit && limitLength && e.target.value.length > limitLength) {
return;
Expand Down Expand Up @@ -103,6 +109,7 @@ const SInput = React.forwardRef<HTMLInputElement, InputProps<any>>(
onClick={handleIconClick} // 아이콘 클릭 이벤트 추가
/>
)}
{renderDropdown && <div className="relative">{renderDropdown()}</div>}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { ProjectRegisterRequest } from '@/service/project/request';
import getErrorMessage from '@/utils/getErrorMessage';
import { useFormContext } from 'react-hook-form';

export const RegiseterErrorMessage = ({ errorKey }: { errorKey: string }) => {
interface RegisterErrorMessageProps {
errorKey: string;
}

export const RegisterErrorMessage = ({ errorKey }: RegisterErrorMessageProps) => {
const {
formState: { errors }
} = useFormContext<ProjectRegisterRequest>();
} = useFormContext();

const errorMessage = getErrorMessage(errors, errorKey);

if (!errorMessage) {
Expand Down
9 changes: 7 additions & 2 deletions src/components/common/SVGIcon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export type IconType =
| 'toast_info'
| 'toast_success'
| 'toast_error'
| 'refresh';
| 'refresh'
| 'minus';

type Props = {
icon: IconType;
Expand Down Expand Up @@ -78,7 +79,8 @@ const VIEWBOX_MAP: { [key: string]: { width: number; height: number } } = {
tooltip: { width: 16, height: 16 },
toast_info: { width: 24, height: 24 },
toast_success: { width: 24, height: 24 },
toast_error: { width: 24, height: 24 }
toast_error: { width: 24, height: 24 },
minus: { width: 16, height: 16 }
};

const ICONS: { [key in IconType]: string[] } = {
Expand Down Expand Up @@ -161,6 +163,9 @@ const ICONS: { [key in IconType]: string[] } = {
hamburger: [
'M5.5 7.86217C5.5 7.35239 5.88029 7 6.24331 7H17.7576C18.1206 7 18.5 7.35239 18.5 7.86217C18.5 8.37196 18.1206 8.71857 17.7576 8.71857H6.24331C5.88029 8.71857 5.5 8.37196 5.5 7.86217ZM5.5 16.1476C5.5 15.6378 5.88029 15.2814 6.24331 15.2814H17.7576C18.1206 15.2814 18.5 15.6378 18.5 16.1476C18.5 16.6573 18.1206 17 17.7576 17H6.24331C5.88029 17 5.5 16.6573 5.5 16.1476Z',
'M5.5 12.0049C5.5 11.4951 5.88029 11.1457 6.24331 11.1457H17.7576C18.1206 11.1457 18.5 11.4951 18.5 12.0049C18.5 12.5147 18.1206 12.8542 17.7576 12.8542H6.24331C5.88029 12.8542 5.5 12.5147 5.5 12.0049Z'
],
minus: [
'M3.40234 8.00039C3.40234 7.66902 3.67097 7.40039 4.00234 7.40039H12.0023C12.3337 7.40039 12.6023 7.66902 12.6023 8.00039C12.6023 8.33176 12.3337 8.60039 12.0023 8.60039H4.00234C3.67097 8.60039 3.40234 8.33176 3.40234 8.00039Z'
]
};

Expand Down
45 changes: 35 additions & 10 deletions src/components/common/Textarea/index.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,67 @@
'use client';

import React, { useState } from 'react';
import {
FieldValues,
Path,
RegisterOptions,
UseFormRegister,
UseFormRegisterReturn
} from 'react-hook-form';

interface TextareaProps {
interface TextareaProps<T extends FieldValues = FieldValues> {
className?: string;
placeholder?: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
maxLength?: number; // 최대 글자 수 설정
name?: string;
maxLength?: number;
name?: Path<T>;
register?: UseFormRegister<T>;
registerOptions?: RegisterOptions<T>;
}

const STextarea = ({
const STextarea = <T extends FieldValues = FieldValues>({
className = '',
placeholder,
value,
name = '',
name,
onChange,
maxLength = 1000
}: TextareaProps) => {
maxLength = 1000,
register,
registerOptions
}: TextareaProps<T>) => {
const [currentLength, setCurrentLength] = useState(value?.length || 0);

const {
onChange: registerOnChange,
ref: registerRef,
...restRegister
}: UseFormRegisterReturn | { ref?: undefined; onChange?: undefined } = register && name
? register(name, registerOptions)
: {};

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setCurrentLength(e.target.value.length); // 현재 글자 수 업데이트
if (onChange && currentLength < maxLength) {
onChange(e); // 외부 onChange 이벤트 호출
const val = e.target.value;
setCurrentLength(val.length);

if (val.length <= maxLength) {
[registerOnChange, onChange].forEach((fn) => {
if (fn) fn(e);
});
}
};

return (
<div className="flex flex-col gap-1.5 bg-white rounded-lg">
<textarea
ref={registerRef}
className={`px-4 py-2 border border-slate-300 text-base rounded-lg focus:outline-none focus:ring-2 focus:ring-tree-300 resize-none w-full h-60 ${className}`}
placeholder={placeholder}
value={value}
onChange={handleChange}
maxLength={maxLength}
name={name}
{...restRegister}
/>
{maxLength && (
<div className="text-xsmall text-right text-slate-60">
Expand Down
Loading
Loading