Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 47 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@hookform/resolvers": "^5.0.1",
"axios": "^1.9.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
Expand All @@ -20,8 +21,10 @@
"react-calendar": "^5.1.0",
"react-dom": "^19.0.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.4",
"react-intersection-observer": "^9.16.0",
"react-toastify": "^11.0.5"
"react-toastify": "^11.0.5",
"zod": "^3.25.41"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
Expand Down
32 changes: 32 additions & 0 deletions src/components/common/formField/Fields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import clsx from 'clsx';
import { GAP_SIZE, LABEL_SIZE } from './style';
import { FieldProps } from './type';

export default function Fields({
label,
required,
errorMessage,
gapSize = '12',
labelSize = '16/16',
render,
}: FieldProps) {
const showError = !!errorMessage;

return (
<div className="flex w-full flex-col gap-2">
<div className={clsx('flex flex-col', GAP_SIZE[gapSize])}>
{label && (
<label className={clsx('flex gap-1.5', LABEL_SIZE[labelSize])}>
{required && <span className="text-tertiary text-2lg-bold sm:text-xl-bold">*</span>}
{label}
</label>
)}
{render()}
</div>

{showError && <span className="text-danger text-md-md text-left">{errorMessage}</span>}
</div>
);
}
4 changes: 2 additions & 2 deletions src/components/common/formField/compound/ImageUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ export default function ImageUploader({ imageUploaderType, image, inputRef }: Im
<button
onClick={triggerUploadClick}
className={clsx(
'border-border relative flex h-16 w-16 cursor-pointer items-center justify-center rounded-full border-2',
'border-border relative flex h-16 w-16 cursor-pointer items-center justify-center overflow-hidden rounded-full border-2',
TEAM ? 'bg-bg200' : 'bg-bg100'
)}
>
{image ? (
<Image src={image} fill alt="profile" className="rounded-full" />
<Image src={image} fill alt="profile" className="object-cover" />
) : (
<Image
src={defaultIcon}
Expand Down
3 changes: 2 additions & 1 deletion src/components/common/formField/compound/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { InputProps } from '../type';
export default function Input({
leftSlot = null,
rightSlot = null,
hasError,
borderClassName = 'border-border',
className,
ref,
Expand All @@ -16,7 +17,7 @@ export default function Input({
<div
className={clsx(
'bg-bg200 flex h-11 w-full gap-3 rounded-xl border px-4 py-2.5 sm:h-12',
borderClassName,
hasError ? 'border-danger' : borderClassName,
className
)}
>
Expand Down
1 change: 1 addition & 0 deletions src/components/common/formField/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const LABEL_SIZE = {
'16/20': 'text-lg-md sm:text-xl-bold',
} as const;

// μ‚­μ œ μ˜ˆμ •
export const getBorderClassName = ({
isFocused,
showSuccess,
Expand Down
19 changes: 17 additions & 2 deletions src/components/common/formField/type.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { InputHTMLAttributes, TextareaHTMLAttributes } from 'react';
import { GAP_SIZE, LABEL_SIZE } from './style';

export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
leftSlot?: React.ReactNode;
rightSlot?: React.ReactNode;
hasError?: boolean;
borderClassName?: string;
className?: string;
ref?: React.Ref<HTMLInputElement>;
Expand All @@ -15,14 +17,17 @@ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElemen
className?: string;
}

// μ‚­μ œ μ˜ˆμ •
export type ImageUploaderType = 'board' | 'team' | 'user';

export interface FileInputProps extends InputHTMLAttributes<HTMLInputElement> {
// imageUploaderType μ‚­μ œ μ˜ˆμ •
imageUploaderType?: ImageUploaderType;
image: string | null;
onImageChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

// μ‚­μ œ μ˜ˆμ •
export interface FormFieldProps {
field: 'input' | 'textarea' | 'file-input';
imageUploaderType?: ImageUploaderType;
Expand All @@ -34,10 +39,20 @@ export interface FormFieldProps {
errorMessage?: string;
gapSize?: '12' | '16' | '24' | '32';
labelSize?: '16/16' | '14/16' | '16/20';
onFieldFocus?: () => void;
onFieldBlur?: () => void;
onFieldFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
onFieldBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
}

export interface FieldProps {
label?: string;
required?: boolean;
errorMessage?: string;
gapSize?: keyof typeof GAP_SIZE;
labelSize?: keyof typeof LABEL_SIZE;
render: () => React.ReactNode;
}

// μ‚­μ œ 에정
export type FieldComponentProps =
| (InputProps & FormFieldProps & { field?: 'input' })
| (TextareaProps & FormFieldProps & { field?: 'textarea' })
Expand Down
78 changes: 41 additions & 37 deletions src/components/manage-group/ManageGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,70 @@
'use client';

import Button from '@/components/common/Button';
import FormField from '@/components/common/formField';
import Fields from '../common/formField/Fields';
import Input from '../common/formField/compound/Input';
import FileInput from '../common/formField/compound/FileInput';
import ImageUploader from '../common/formField/compound/ImageUploader';
import BouncingDots from '@/components/common/loading/BouncingDots';
import useManageGroup from './useManageGroup';
import { Group } from '@/types/group';

export type ManageGroup = Partial<Pick<Group, 'id'>> & Pick<Group, 'name' | 'image'>;

interface MangeGroupProps {
interface ManageGroupProps {
groupData?: ManageGroup;
groupNames: string[];
}

export default function ManageGroup({ groupData, groupNames }: MangeGroupProps) {
export default function ManageGroup({ groupData, groupNames }: ManageGroupProps) {
const isEdit = !!groupData;
const groupButtonText = isEdit ? 'μˆ˜μ •ν•˜κΈ°' : 'μƒμ„±ν•˜κΈ°';

const {
group,
isNameFailure,
isImageEmpty,
isSubmit,
form: {
register,
handleSubmit,
formState: { errors },
},
image,
isPending,
imageErrorMessage,
nameErrorMessage,
handleNameChange,
handleImageChange,
handleManageGroupSubmit,
} = useManageGroup({
isEdit,
groupData,
groupNames,
});

const groupButtonText = isEdit ? 'μˆ˜μ •ν•˜κΈ°' : 'μƒμ„±ν•˜κΈ°';
handleSubmitGroup,
} = useManageGroup({ isEdit, groupData, groupNames });

return (
<form onSubmit={handleManageGroupSubmit} className="flex w-full flex-col gap-10">
<form onSubmit={handleSubmit(handleSubmitGroup)} className="flex w-full flex-col gap-10">
<div className="flex w-full flex-col gap-6">
<FormField
field="file-input"
name="image"
<Fields
label="νŒ€ ν”„λ‘œν•„"
imageUploaderType="team"
isFailure={isImageEmpty}
isSubmit={isSubmit}
errorMessage={imageErrorMessage()}
image={group.image}
onImageChange={handleImageChange}
errorMessage={errors.image?.message}
render={() => (
<FileInput name="image" onImageChange={handleImageChange}>
{({ inputRef }) => (
<ImageUploader image={image} imageUploaderType="team" inputRef={inputRef} />
)}
</FileInput>
)}
/>
<FormField
field="input"
name="name"

<Fields
label="νŒ€ 이름"
placeholder="νŒ€ 이름을 μž…λ ₯ν•΄ μ£Όμ„Έμš”."
isFailure={isNameFailure}
isSubmit={isSubmit}
errorMessage={nameErrorMessage()}
value={group.name}
onChange={handleNameChange}
errorMessage={errors.name?.message}
render={() => {
const { onBlur, ...inputProps } = register('name');
return (
<Input
{...inputProps}
name="name"
placeholder="νŒ€ 이름을 μž…λ ₯ν•΄ μ£Όμ„Έμš”."
onBlur={onBlur}
hasError={!!errors.name}
/>
);
}}
/>
</div>

<div className="flex flex-col gap-6">
<Button type="submit" variant="solid" size="fullWidth" disabled={isPending}>
{isPending ? <BouncingDots /> : groupButtonText}
Expand Down
Loading