From 3965a01ef75440c624e124c2e56cc05f67004a43 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 01:33:07 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20input=20disable=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=8B=9C=20=EC=BB=A4=EC=8A=A4=ED=85=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/input/base.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/shared/ui/input/base.tsx b/src/shared/ui/input/base.tsx index c0bbffae..1d6d667b 100644 --- a/src/shared/ui/input/base.tsx +++ b/src/shared/ui/input/base.tsx @@ -40,6 +40,9 @@ const inputVariants = cva( boxed: 'rounded-100 border px-150 border-border-default', bare: 'border-0 rounded-none px-0 focus-visible:outline-none shadow-none', }, + disabled: { + true: 'bg-background-disabled text-text-disabled border-border-disabled cursor-not-allowed', + }, }, defaultVariants: { color: 'default', @@ -64,7 +67,16 @@ export const BaseInput = React.forwardRef( return ( { onChange?.(e); From 71a4f5ac3b31eb51b4dba9b2568a11145fe1ab9b Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 01:33:19 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20=EB=8B=A8=EC=9D=BC=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/toggle/group.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/shared/ui/toggle/group.tsx b/src/shared/ui/toggle/group.tsx index 45076634..1b090346 100644 --- a/src/shared/ui/toggle/group.tsx +++ b/src/shared/ui/toggle/group.tsx @@ -9,15 +9,26 @@ export interface ToggleGroupProps { options: ToggleOption[]; value?: string[]; onChange?: (v: string[]) => void; + multiple?: boolean; } -function ToggleGroup({ options, value, onChange }: ToggleGroupProps) { +function ToggleGroup({ + options, + value, + onChange, + multiple = true, +}: ToggleGroupProps) { const selected = value ?? []; const toggle = (key: string) => { - const next = selected.includes(key) - ? selected.filter((x) => x !== key) - : [...selected, key]; + let next: string[]; + if (multiple) { + next = selected.includes(key) + ? selected.filter((x) => x !== key) + : [...selected, key]; + } else { + next = selected.includes(key) ? [] : [key]; + } onChange?.(next); }; From 33dc7c3a1fc2d894cb2a7c2e446e0b0196c67182 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 01:38:15 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A3=B9=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EC=88=98=EC=A0=95=ED=95=84=EC=9A=94)=20=EB=B0=B1?= =?UTF-8?q?=EC=97=94=EB=93=9C=20api=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=20=ED=9B=84=20=EC=88=98=EC=A0=95=EC=9D=B4=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/model/open-group-form.schema.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/features/study/group/model/open-group-form.schema.ts diff --git a/src/features/study/group/model/open-group-form.schema.ts b/src/features/study/group/model/open-group-form.schema.ts new file mode 100644 index 00000000..a6c97940 --- /dev/null +++ b/src/features/study/group/model/open-group-form.schema.ts @@ -0,0 +1,65 @@ +import { z } from 'zod'; +import { OpenGroupRequest } from '../api/group-types'; +import { + EXPERIENCE_LEVEL_OPTIONS, + METHOD_OPTIONS, + REGULAR_MEETING_OPTIONS, + TYPE_OPTIONS, +} from '../const/group-const'; + +// YYYY-MM-DD 형식 검증 +const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; + +export const OpenGroupFormSchema = z.object({ + type: z.enum(TYPE_OPTIONS), + targetRole: z.array(z.string()).min(1, '역할을 1개 이상 선택해 주세요.'), + maxMembers: z + .string() + .trim() + .regex(/^[1-9]\d*$/, '최소 1 이상의 정수를 입력해 주세요.'), + experienceLevel: z.enum(EXPERIENCE_LEVEL_OPTIONS), + method: z.enum(METHOD_OPTIONS), + regularMeeting: z.enum(REGULAR_MEETING_OPTIONS), + startDate: z + .string() + .trim() + .regex(ISO_DATE_REGEX, 'YYYY-MM-DD 형식의 시작일을 입력해 주세요.'), + durationWeeks: z + .string() + .trim() + .regex(/^[1-9]\d*$/, '기간은 1 이상 정수로 입력해 주세요.'), + price: z + .string() + .trim() + .regex(/^\d+$/, '가격은 0 이상 정수로 입력해 주세요.'), +}); + +export type OpenGroupFormValues = z.infer; + +export function buildOpenGroupDefaultValues(): OpenGroupFormValues { + return { + type: '프로젝트', + targetRole: [], + maxMembers: '', + experienceLevel: '주니어', + method: '온라인', + regularMeeting: '주1회', + startDate: '', + durationWeeks: '', + price: '', + }; +} + +export function toOpenGroupRequest(v: OpenGroupFormValues): OpenGroupRequest { + return { + type: v.type, + targetRole: v.targetRole.join(','), + maxMembers: Number(v.maxMembers), + experienceLevel: v.experienceLevel, + method: v.method, + regularMeeting: v.regularMeeting, + startDate: v.startDate.trim(), + durationWeeks: Number(v.durationWeeks), + price: Number(v.price), + }; +} From 881e717dd09c810f40560b48a25cafdc7894e378 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 01:38:27 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A3=B9=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=EC=83=9D=EC=84=B1=20const=20=EA=B0=92=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/group/const/group-const.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/features/study/group/const/group-const.ts diff --git a/src/features/study/group/const/group-const.ts b/src/features/study/group/const/group-const.ts new file mode 100644 index 00000000..9d32fac3 --- /dev/null +++ b/src/features/study/group/const/group-const.ts @@ -0,0 +1,30 @@ +export const TYPE_OPTIONS = [ + '프로젝트', + '멘토링', + '세미나', + '챌린지', + '책스터디', + '강의스터디', +] as const; + +export const EXPERIENCE_LEVEL_OPTIONS = [ + '입문자', + '취준생', + '주니어', + '미들', + '시니어', +] as const; + +export const METHOD_OPTIONS = ['온라인', '오프라인', '온오프라인'] as const; + +export const REGULAR_MEETING_OPTIONS = [ + '없음', + '주1회', + '주2회', + '주3회이상', +] as const; + +export type TypeOption = (typeof TYPE_OPTIONS)[number]; +export type ExperienceLevelOption = (typeof EXPERIENCE_LEVEL_OPTIONS)[number]; +export type MethodOption = (typeof METHOD_OPTIONS)[number]; +export type RegularMeetingOption = (typeof REGULAR_MEETING_OPTIONS)[number]; From 116fcf790a9f05e72077955f894a6a044dfeb6d8 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 01:38:49 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A3=B9=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20request=20=ED=83=80=EC=9E=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EC=88=98=EC=A0=95=20=ED=95=84=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/group/api/group-types.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/features/study/group/api/group-types.ts diff --git a/src/features/study/group/api/group-types.ts b/src/features/study/group/api/group-types.ts new file mode 100644 index 00000000..df93bd1f --- /dev/null +++ b/src/features/study/group/api/group-types.ts @@ -0,0 +1,18 @@ +import { + ExperienceLevelOption, + MethodOption, + RegularMeetingOption, + TypeOption, +} from '../const/group-const'; + +export interface OpenGroupRequest { + type: TypeOption; + targetRole: string; + maxMembers: number; + experienceLevel: ExperienceLevelOption; + method: MethodOption; + regularMeeting: RegularMeetingOption; + startDate: string; + durationWeeks: number; + price: number; +} From 2093d75f53de05da9e76e08e1d24d0edc1b472fe Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 03:08:56 +0900 Subject: [PATCH 06/20] =?UTF-8?q?fix:=20=EA=B2=BD=EB=A0=A5=20const=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/group/const/group-const.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features/study/group/const/group-const.ts b/src/features/study/group/const/group-const.ts index 9d32fac3..6688b217 100644 --- a/src/features/study/group/const/group-const.ts +++ b/src/features/study/group/const/group-const.ts @@ -10,9 +10,10 @@ export const TYPE_OPTIONS = [ export const EXPERIENCE_LEVEL_OPTIONS = [ '입문자', '취준생', - '주니어', - '미들', - '시니어', + '주니어 (1~3년차)', + '중니어 (4~6년차)', + '시니어 (7년차 이상)', + '경력 무관', ] as const; export const METHOD_OPTIONS = ['온라인', '오프라인', '온오프라인'] as const; From 52760269b5a3b7398bd4a016f9fc46ce779615fc Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 03:09:06 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat:=20=EC=8A=A4=ED=85=9D=201=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/ui/step/step1-group.tsx | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/features/study/group/ui/step/step1-group.tsx diff --git a/src/features/study/group/ui/step/step1-group.tsx b/src/features/study/group/ui/step/step1-group.tsx new file mode 100644 index 00000000..da22ddd2 --- /dev/null +++ b/src/features/study/group/ui/step/step1-group.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useState } from 'react'; +import { SingleDropdown } from '@/shared/ui/dropdown'; +import FormField from '@/shared/ui/form/form-field'; +import { BaseInput } from '@/shared/ui/input'; +import { ToggleGroup } from '@/shared/ui/toggle'; +import { + EXPERIENCE_LEVEL_OPTIONS, + METHOD_OPTIONS, + REGULAR_MEETING_OPTIONS, + TYPE_OPTIONS, +} from '../../const/group-const'; +import { OpenGroupFormValues } from '../../model/open-group-form.schema'; + +const ROLE_OPTIONS = [ + { label: '백엔드', value: 'backend' }, + { label: '프론트엔드', value: 'frontend' }, + { label: '기획', value: 'planner' }, + { label: 'UX/UI 디자이너', value: 'uxui-designer' }, +]; + +const typeOptions = TYPE_OPTIONS.map((v) => ({ label: v, value: v })); +const expOptions = EXPERIENCE_LEVEL_OPTIONS.map((v) => ({ + label: v, + value: v, +})); +const methodOptions = METHOD_OPTIONS.map((v) => ({ label: v, value: v })); +const meetingOptions = REGULAR_MEETING_OPTIONS.map((v) => ({ + label: v, + value: v, +})); + +export default function Step1GroupStudy() { + const [method, setMethod] = useState(undefined); + + return ( + <> +
기본 정보 설정
+ + name="type" + label="스터디 유형" + helper="어떤 방식으로 진행되는 스터디인지 선택해주세요." + direction="vertical" + required + > + {/* todo: 여기 radio 변경 */} + + + + + name="targetRole" + label="모집 대상" + helper="함께하고 싶은 대상(직무·관심사 등)을 선택해주세요. (복수 선택 가능)" + direction="vertical" + required + > + + + + + name="maxMembers" + label="모집 인원" + // todo: 여기 description 변경되면 수정 필요 + helper="모집 인원을 입력해 주세요." + direction="vertical" + required + > + + + + + name="experienceLevel" + label="경력 여부" + helper="함께할 구성원의 경력 레벨을 선택해 주세요." + direction="vertical" + required + > + + + + + name="method" + label="진행 방식" + helper="스터디가 진행되는 방식을 선택해주세요. (예: 온라인, 오프라인 등)" + direction="vertical" + required + > +
+ + +
+ + + + name="regularMeeting" + label="정기 모임" + helper="정기적으로 모일 빈도를 선택해주세요." + direction="vertical" + required + > + {/* todo: 여기 radio 변경 */} + + + + + name="startDate" + label="진행 기간" + helper="스터디가 운영될 기간을 입력해주세요." + direction="vertical" + required + > +
+ +
~
+ +
+ + + ); +} From 4e5290769e10a62a6344c2a3ecab9b74f2d59630 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 03:09:20 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20=EC=8A=A4=ED=85=9D=202=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/ui/step/step2-group.tsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/features/study/group/ui/step/step2-group.tsx diff --git a/src/features/study/group/ui/step/step2-group.tsx b/src/features/study/group/ui/step/step2-group.tsx new file mode 100644 index 00000000..78ff5254 --- /dev/null +++ b/src/features/study/group/ui/step/step2-group.tsx @@ -0,0 +1,51 @@ +'use client'; + +import FormField from '@/shared/ui/form/form-field'; +import { BaseInput, TextAreaInput } from '@/shared/ui/input'; +import { OpenGroupFormValues } from '../../model/open-group-form.schema'; + +export default function Step2GroupStudy() { + // todo: 그룹 폼 필드 변경해야 함 + return ( + <> +
+ 스터디 소개 작성 +
+ + name="type" + label="썸네일" + direction="vertical" + required + > + {/* todo: 여기 파일 업로드 변경 */} + + + + name="type" + label="스터디 제목" + direction="vertical" + required + > + + + + + name="type" + label="스터디 한 줄 소개" + direction="vertical" + required + > + + + + + name="type" + label="스터디 소개" + direction="vertical" + required + > + + + + ); +} From 4315d08d9df76c8de3ff0acdf1d2448ad43aaf2e Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 03:09:56 +0900 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20=EA=B2=BD=EB=A0=A5=20=EC=9D=B8?= =?UTF-8?q?=ED=92=8B/=EC=95=84=EC=9B=83=ED=92=8B=20=EB=8B=A4=EB=A5=B4?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/model/open-group-form.schema.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/features/study/group/model/open-group-form.schema.ts b/src/features/study/group/model/open-group-form.schema.ts index a6c97940..9afc32d1 100644 --- a/src/features/study/group/model/open-group-form.schema.ts +++ b/src/features/study/group/model/open-group-form.schema.ts @@ -9,6 +9,7 @@ import { // YYYY-MM-DD 형식 검증 const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; +const ExperienceLevelEnum = z.enum(EXPERIENCE_LEVEL_OPTIONS); export const OpenGroupFormSchema = z.object({ type: z.enum(TYPE_OPTIONS), @@ -17,7 +18,10 @@ export const OpenGroupFormSchema = z.object({ .string() .trim() .regex(/^[1-9]\d*$/, '최소 1 이상의 정수를 입력해 주세요.'), - experienceLevel: z.enum(EXPERIENCE_LEVEL_OPTIONS), + experienceLevel: z + .union([ExperienceLevelEnum, z.literal('')]) + .transform((v) => (v === '' ? undefined : v)) + .pipe(ExperienceLevelEnum), method: z.enum(METHOD_OPTIONS), regularMeeting: z.enum(REGULAR_MEETING_OPTIONS), startDate: z @@ -34,14 +38,15 @@ export const OpenGroupFormSchema = z.object({ .regex(/^\d+$/, '가격은 0 이상 정수로 입력해 주세요.'), }); -export type OpenGroupFormValues = z.infer; +export type OpenGroupFormValues = z.input; +export type OpenGroupParsedValues = z.output; export function buildOpenGroupDefaultValues(): OpenGroupFormValues { return { type: '프로젝트', targetRole: [], maxMembers: '', - experienceLevel: '주니어', + experienceLevel: '', method: '온라인', regularMeeting: '주1회', startDate: '', @@ -50,7 +55,7 @@ export function buildOpenGroupDefaultValues(): OpenGroupFormValues { }; } -export function toOpenGroupRequest(v: OpenGroupFormValues): OpenGroupRequest { +export function toOpenGroupRequest(v: OpenGroupParsedValues): OpenGroupRequest { return { type: v.type, targetRole: v.targetRole.join(','), From 6376a90f1f295e59f91b7bc814d183aa33d2dddd Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 03:13:42 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20=EC=8A=A4=ED=85=9D=203=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/ui/step/step3-group.tsx | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/features/study/group/ui/step/step3-group.tsx diff --git a/src/features/study/group/ui/step/step3-group.tsx b/src/features/study/group/ui/step/step3-group.tsx new file mode 100644 index 00000000..db8a1101 --- /dev/null +++ b/src/features/study/group/ui/step/step3-group.tsx @@ -0,0 +1,42 @@ +'use client'; + +import FormField from '@/shared/ui/form/form-field'; +import { TextAreaInput } from '@/shared/ui/input'; +import { OpenGroupFormValues } from '../../model/open-group-form.schema'; + +export default function Step3GroupStudy() { + // todo: 그룹 폼 필드 변경해야 함 + return ( + <> +
+ 지원 & 규칙 설정 +
+ + name="targetRole" + label="스터디원에게 보여줄 질문을 입력하세요" + helper="스터디 지원자가 신청 시 작성해야 할 질문을 설정하세요. (예: 지원 동기, 경험, 기대하는 점 등)" + direction="vertical" + required + > + + + +
+
+ 스터디 리더님, 개설 규칙을 준수해주세요. +
+
    +
  • 모집 공고의 정보는 사실에 기반해 작성해야 합니다.
  • +
  • 진행 기간과 모임 빈도를 명확히 안내해야 합니다.
  • +
  • 스터디 개설 후 최소 1회 이상 정기 모임을 진행해야 합니다.
  • +
  • 스터디원과의 약속을 존중하고 성실히 운영해야 합니다.
  • +
  • + 타인의 개인정보 및 자료를 무단으로 공유하거나 외부 유출해서는 안 + 됩니다. +
  • +
  • 플랫폼의 운영 정책 및 커뮤니티 가이드를 준수해야 합니다.
  • +
+
+ + ); +} From c5b14f3dfad51f8e0695bca1e93d2f00b92c76dd Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 03:22:51 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat:=20=ED=8F=B0=ED=8A=B8=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/ui/step/step1-group.tsx | 7 +++++ .../study/group/ui/step/step2-group.tsx | 4 +++ .../study/group/ui/step/step3-group.tsx | 1 + src/shared/ui/form/form-field.tsx | 31 ++++++++++++++++--- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/features/study/group/ui/step/step1-group.tsx b/src/features/study/group/ui/step/step1-group.tsx index da22ddd2..d9353046 100644 --- a/src/features/study/group/ui/step/step1-group.tsx +++ b/src/features/study/group/ui/step/step1-group.tsx @@ -42,6 +42,7 @@ export default function Step1GroupStudy() { label="스터디 유형" helper="어떤 방식으로 진행되는 스터디인지 선택해주세요." direction="vertical" + scale="medium" required > {/* todo: 여기 radio 변경 */} @@ -53,6 +54,7 @@ export default function Step1GroupStudy() { label="모집 대상" helper="함께하고 싶은 대상(직무·관심사 등)을 선택해주세요. (복수 선택 가능)" direction="vertical" + scale="medium" required > @@ -64,6 +66,7 @@ export default function Step1GroupStudy() { // todo: 여기 description 변경되면 수정 필요 helper="모집 인원을 입력해 주세요." direction="vertical" + scale="medium" required > @@ -74,6 +77,7 @@ export default function Step1GroupStudy() { label="경력 여부" helper="함께할 구성원의 경력 레벨을 선택해 주세요." direction="vertical" + scale="medium" required > @@ -84,6 +88,7 @@ export default function Step1GroupStudy() { label="진행 방식" helper="스터디가 진행되는 방식을 선택해주세요. (예: 온라인, 오프라인 등)" direction="vertical" + scale="medium" required >
@@ -102,6 +107,7 @@ export default function Step1GroupStudy() { label="정기 모임" helper="정기적으로 모일 빈도를 선택해주세요." direction="vertical" + scale="medium" required > {/* todo: 여기 radio 변경 */} @@ -113,6 +119,7 @@ export default function Step1GroupStudy() { label="진행 기간" helper="스터디가 운영될 기간을 입력해주세요." direction="vertical" + scale="medium" required >
diff --git a/src/features/study/group/ui/step/step2-group.tsx b/src/features/study/group/ui/step/step2-group.tsx index 78ff5254..25f058c4 100644 --- a/src/features/study/group/ui/step/step2-group.tsx +++ b/src/features/study/group/ui/step/step2-group.tsx @@ -15,6 +15,7 @@ export default function Step2GroupStudy() { name="type" label="썸네일" direction="vertical" + scale="medium" required > {/* todo: 여기 파일 업로드 변경 */} @@ -24,6 +25,7 @@ export default function Step2GroupStudy() { name="type" label="스터디 제목" direction="vertical" + scale="medium" required > @@ -33,6 +35,7 @@ export default function Step2GroupStudy() { name="type" label="스터디 한 줄 소개" direction="vertical" + scale="medium" required > @@ -42,6 +45,7 @@ export default function Step2GroupStudy() { name="type" label="스터디 소개" direction="vertical" + scale="medium" required > diff --git a/src/features/study/group/ui/step/step3-group.tsx b/src/features/study/group/ui/step/step3-group.tsx index db8a1101..fe95702b 100644 --- a/src/features/study/group/ui/step/step3-group.tsx +++ b/src/features/study/group/ui/step/step3-group.tsx @@ -16,6 +16,7 @@ export default function Step3GroupStudy() { label="스터디원에게 보여줄 질문을 입력하세요" helper="스터디 지원자가 신청 시 작성해야 할 질문을 설정하세요. (예: 지원 동기, 경험, 기대하는 점 등)" direction="vertical" + scale="medium" required > diff --git a/src/shared/ui/form/form-field.tsx b/src/shared/ui/form/form-field.tsx index a922c6f7..5cebba69 100644 --- a/src/shared/ui/form/form-field.tsx +++ b/src/shared/ui/form/form-field.tsx @@ -13,6 +13,22 @@ import { cn } from '@/shared/shadcn/lib/utils'; import { FieldControl, type ControlledChildProps } from './field-control'; type Direction = 'horizontal' | 'vertical'; +type Scale = 'small' | 'medium'; + +const TYPO = { + small: { + label: 'font-designer-14b', + helper: 'font-designer-14r', + foot: 'font-designer-13r', + counter: 'font-designer-13r', + }, + medium: { + label: 'font-designer-16b', + helper: 'font-designer-14r', + foot: 'font-designer-14r', + counter: 'font-designer-13r', + }, +} as const; export interface FormFieldProps< T extends FieldValues, @@ -27,6 +43,7 @@ export interface FormFieldProps< description?: React.ReactNode; required?: boolean; direction?: Direction; + scale?: Scale; id?: string; showCounterRight?: boolean; @@ -47,6 +64,7 @@ export default function FormField< description, required = false, direction = 'horizontal', + scale = 'small', id, children, showCounterRight = false, @@ -79,7 +97,7 @@ export default function FormField<
@@ -90,7 +108,7 @@ export default function FormField<
{helper && ( -
+
{helper}
)} @@ -104,10 +122,13 @@ export default function FormField< {children} -
+
@@ -115,7 +136,7 @@ export default function FormField<
{showCounterRight && typeof counterMax === 'number' && ( -
+
{currentLen}/{counterMax}
)} From c28518514a425ece7c7ff7eb12a8d7eac73ba397 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 03:23:53 +0900 Subject: [PATCH 12/20] =?UTF-8?q?fix:=20group=20=EB=8B=A8=EC=9D=BC/?= =?UTF-8?q?=EB=B3=B5=EC=88=98=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=8B=A4?= =?UTF-8?q?=EB=A5=B8=20=ED=83=80=EC=9E=85=20=ED=99=95=EC=9E=A5=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/toggle/group.tsx | 74 +++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/src/shared/ui/toggle/group.tsx b/src/shared/ui/toggle/group.tsx index 1b090346..7df7c946 100644 --- a/src/shared/ui/toggle/group.tsx +++ b/src/shared/ui/toggle/group.tsx @@ -1,3 +1,4 @@ +import type { ComponentType, ReactNode } from 'react'; import ToggleButton from './button'; export interface ToggleOption { @@ -5,31 +6,63 @@ export interface ToggleOption { label: string; } -export interface ToggleGroupProps { +interface MultiProps { options: ToggleOption[]; + multiple?: true; value?: string[]; onChange?: (v: string[]) => void; - multiple?: boolean; + renderItem?: ComponentType; } -function ToggleGroup({ - options, - value, - onChange, - multiple = true, -}: ToggleGroupProps) { - const selected = value ?? []; +interface SingleProps { + options: ToggleOption[]; + multiple: false; + value?: string; + onChange?: (v: string | undefined) => void; + allowDeselect?: boolean; + emptyValue?: string | undefined; + renderItem?: ComponentType; +} + +export type ToggleGroupProps = MultiProps | SingleProps; + +export interface ItemRendererProps { + pressed: boolean; + onPress: () => void; + children: ReactNode; +} + +function DefaultToggleItem({ pressed, onPress, children }: ItemRendererProps) { + return ( + + {children} + + ); +} + +function ToggleGroup(props: ToggleGroupProps) { + const { options } = props; + const isMulti = props.multiple !== false; + const Item = props.renderItem ?? DefaultToggleItem; + + const selectedSet = new Set( + isMulti ? (props.value ?? []) : props.value ? [props.value] : [], + ); const toggle = (key: string) => { - let next: string[]; - if (multiple) { - next = selected.includes(key) - ? selected.filter((x) => x !== key) - : [...selected, key]; + if (isMulti) { + const curr = (props.value ?? []) as string[]; + const next = curr.includes(key) + ? curr.filter((x) => x !== key) + : [...curr, key]; + (props.onChange as ((v: string[]) => void) | undefined)?.(next); } else { - next = selected.includes(key) ? [] : [key]; + const curr = props.value as string | undefined; + const allowDeselect = props.allowDeselect ?? true; + const cleared = 'emptyValue' in props ? props.emptyValue : undefined; + const next = curr === key ? (allowDeselect ? cleared : key) : key; + (props.onChange as ((v: string | undefined) => void) | undefined)?.(next); } - onChange?.(next); }; return ( @@ -39,14 +72,9 @@ function ToggleGroup({ aria-label="toggle-group" > {options.map(({ value: v, label }) => ( - toggle(v)} - > + toggle(v)}> {label} - + ))}
); From 181ad38552bdd4b5e4d38e3982da39f1abd137fe Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 03:24:16 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20=EC=8A=A4=ED=85=9D=20=EB=B3=84=20?= =?UTF-8?q?=EA=B7=B8=EB=A3=B9=20=EC=8A=A4=ED=84=B0=EB=94=94=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=AA=A8=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/ui/open-group-modal.tsx | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 src/features/study/group/ui/open-group-modal.tsx diff --git a/src/features/study/group/ui/open-group-modal.tsx b/src/features/study/group/ui/open-group-modal.tsx new file mode 100644 index 00000000..f673fec7 --- /dev/null +++ b/src/features/study/group/ui/open-group-modal.tsx @@ -0,0 +1,189 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { XIcon } from 'lucide-react'; +import { useState } from 'react'; +import { FormProvider, useForm, type SubmitHandler } from 'react-hook-form'; + +import Button from '@/shared/ui/button'; +import { Modal } from '@/shared/ui/modal'; + +import { + OpenGroupFormSchema, + type OpenGroupFormValues, + buildOpenGroupDefaultValues, + toOpenGroupRequest, +} from '../model/open-group-form.schema'; +import Step1GroupStudy from './step/step1-group'; +import Step2GroupStudy from './step/step2-group'; +import Step3GroupStudy from './step/step3-group'; + +function Stepper({ step }: { step: 1 | 2 | 3 }) { + const dot = (n: 1 | 2 | 3) => { + const active = step === n; + + return ( +
+ {n} +
+ ); + }; + + return ( +
+ {[1, 2, 3].map((n) => dot(n as 1 | 2 | 3))} +
+ ); +} + +interface OpenGroupStudyModalProps { + memberId: number; + trigger?: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +export default function OpenGroupStudyModal({ + memberId, + trigger, + open, + onOpenChange, +}: OpenGroupStudyModalProps) { + return ( + + {trigger ? {trigger} : null} + + + + + + 그룹 개설하기 + + + + + + onOpenChange?.(false)} /> + + + + ); +} + +function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { + const methods = useForm({ + resolver: zodResolver(OpenGroupFormSchema), + mode: 'onChange', + defaultValues: buildOpenGroupDefaultValues(), + }); + const { handleSubmit, trigger, formState } = methods; + + const [step, setStep] = useState<1 | 2 | 3>(1); + + // 스텝별 검증 필드 지정 + const STEP_FIELDS: Record<1 | 2 | 3, (keyof OpenGroupFormValues)[]> = { + 1: [ + 'type', + 'targetRole', + 'maxMembers', + 'experienceLevel', + 'method', + 'regularMeeting', + 'startDate', + ], + 2: ['type'], + 3: ['price'], + }; + + const goNext = async () => { + const fields = STEP_FIELDS[step]; + const ok = await trigger(fields as any, { shouldFocus: true }); + if (!ok) return; + if (step < 3) setStep((s) => (s + 1) as 1 | 2 | 3); + }; + + const goPrev = () => { + if (step > 1) setStep((s) => (s - 1) as 1 | 2 | 3); + }; + + const onSubmit: SubmitHandler = (values) => { + const req = toOpenGroupRequest(values); + console.log('[DEV] OpenGroupRequest preview:', req); + alert('유효성 통과! (콘솔에서 요청 페이로드 미리보기 확인)'); + onClose(); + }; + + return ( + <> + + + + {...methods}> +
+ {step === 1 && } + {step === 2 && } + {step === 3 && } + + +
+ + +
+ {step > 1 && ( + + )} +
+ +
+ + + + {step < 3 ? ( + + ) : ( + + )} +
+
+ + ); +} From 396dfbcfda1fe0956a4b0d9cf095f0c16860e229 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 03:26:04 +0900 Subject: [PATCH 14/20] =?UTF-8?q?style:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/home/sidebar.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/widgets/home/sidebar.tsx b/src/widgets/home/sidebar.tsx index 2c95571a..c96352de 100644 --- a/src/widgets/home/sidebar.tsx +++ b/src/widgets/home/sidebar.tsx @@ -2,6 +2,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { getUserProfileInServer } from '@/entities/user/api/get-user-profile.server'; import MyProfileCard from '@/entities/user/ui/my-profile-card'; +import OpenGroupStudyModal from '@/features/study/group/ui/open-group-modal'; import StartStudyModal from '@/features/study/participation/ui/start-study-modal'; import { getServerCookie } from '@/shared/lib/server-cookie'; import Calendar from '@/widgets/home/calendar'; @@ -59,6 +60,22 @@ export default async function Sidebar() { } /> )} + {/* todo: 편하게 확인하시라고 추가했습니다. 이후 목록 추가되고 연결되면 제가 지워서 커밋 올리도록 하겠습니다. */} + {/* +

+ + 그룹 스터디를 시작해 보세요! + + + 스터디 신청하기 + +

+ + } + /> */} Date: Thu, 2 Oct 2025 23:19:38 +0900 Subject: [PATCH 15/20] =?UTF-8?q?style:=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/group/ui/step/step1-group.tsx | 16 ++++++++-------- src/features/study/group/ui/step/step2-group.tsx | 10 +++++----- src/features/study/group/ui/step/step3-group.tsx | 4 ++-- src/shared/ui/form/form-field.tsx | 14 +++++++------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/features/study/group/ui/step/step1-group.tsx b/src/features/study/group/ui/step/step1-group.tsx index d9353046..7f2b2a38 100644 --- a/src/features/study/group/ui/step/step1-group.tsx +++ b/src/features/study/group/ui/step/step1-group.tsx @@ -31,7 +31,7 @@ const meetingOptions = REGULAR_MEETING_OPTIONS.map((v) => ({ value: v, })); -export default function Step1GroupStudy() { +export default function Step1OpenGroupStudy() { const [method, setMethod] = useState(undefined); return ( @@ -42,7 +42,7 @@ export default function Step1GroupStudy() { label="스터디 유형" helper="어떤 방식으로 진행되는 스터디인지 선택해주세요." direction="vertical" - scale="medium" + size="medium" required > {/* todo: 여기 radio 변경 */} @@ -54,7 +54,7 @@ export default function Step1GroupStudy() { label="모집 대상" helper="함께하고 싶은 대상(직무·관심사 등)을 선택해주세요. (복수 선택 가능)" direction="vertical" - scale="medium" + size="medium" required > @@ -66,7 +66,7 @@ export default function Step1GroupStudy() { // todo: 여기 description 변경되면 수정 필요 helper="모집 인원을 입력해 주세요." direction="vertical" - scale="medium" + size="medium" required > @@ -77,7 +77,7 @@ export default function Step1GroupStudy() { label="경력 여부" helper="함께할 구성원의 경력 레벨을 선택해 주세요." direction="vertical" - scale="medium" + size="medium" required > @@ -88,7 +88,7 @@ export default function Step1GroupStudy() { label="진행 방식" helper="스터디가 진행되는 방식을 선택해주세요. (예: 온라인, 오프라인 등)" direction="vertical" - scale="medium" + size="medium" required >
@@ -107,7 +107,7 @@ export default function Step1GroupStudy() { label="정기 모임" helper="정기적으로 모일 빈도를 선택해주세요." direction="vertical" - scale="medium" + size="medium" required > {/* todo: 여기 radio 변경 */} @@ -119,7 +119,7 @@ export default function Step1GroupStudy() { label="진행 기간" helper="스터디가 운영될 기간을 입력해주세요." direction="vertical" - scale="medium" + size="medium" required >
diff --git a/src/features/study/group/ui/step/step2-group.tsx b/src/features/study/group/ui/step/step2-group.tsx index 25f058c4..7959ab51 100644 --- a/src/features/study/group/ui/step/step2-group.tsx +++ b/src/features/study/group/ui/step/step2-group.tsx @@ -4,7 +4,7 @@ import FormField from '@/shared/ui/form/form-field'; import { BaseInput, TextAreaInput } from '@/shared/ui/input'; import { OpenGroupFormValues } from '../../model/open-group-form.schema'; -export default function Step2GroupStudy() { +export default function Step2OpenGroupStudy() { // todo: 그룹 폼 필드 변경해야 함 return ( <> @@ -15,7 +15,7 @@ export default function Step2GroupStudy() { name="type" label="썸네일" direction="vertical" - scale="medium" + size="medium" required > {/* todo: 여기 파일 업로드 변경 */} @@ -25,7 +25,7 @@ export default function Step2GroupStudy() { name="type" label="스터디 제목" direction="vertical" - scale="medium" + size="medium" required > @@ -35,7 +35,7 @@ export default function Step2GroupStudy() { name="type" label="스터디 한 줄 소개" direction="vertical" - scale="medium" + size="medium" required > @@ -45,7 +45,7 @@ export default function Step2GroupStudy() { name="type" label="스터디 소개" direction="vertical" - scale="medium" + size="medium" required > diff --git a/src/features/study/group/ui/step/step3-group.tsx b/src/features/study/group/ui/step/step3-group.tsx index fe95702b..7101d523 100644 --- a/src/features/study/group/ui/step/step3-group.tsx +++ b/src/features/study/group/ui/step/step3-group.tsx @@ -4,7 +4,7 @@ import FormField from '@/shared/ui/form/form-field'; import { TextAreaInput } from '@/shared/ui/input'; import { OpenGroupFormValues } from '../../model/open-group-form.schema'; -export default function Step3GroupStudy() { +export default function Step3OpenGroupStudy() { // todo: 그룹 폼 필드 변경해야 함 return ( <> @@ -16,7 +16,7 @@ export default function Step3GroupStudy() { label="스터디원에게 보여줄 질문을 입력하세요" helper="스터디 지원자가 신청 시 작성해야 할 질문을 설정하세요. (예: 지원 동기, 경험, 기대하는 점 등)" direction="vertical" - scale="medium" + size="medium" required > diff --git a/src/shared/ui/form/form-field.tsx b/src/shared/ui/form/form-field.tsx index 5cebba69..683923c1 100644 --- a/src/shared/ui/form/form-field.tsx +++ b/src/shared/ui/form/form-field.tsx @@ -13,7 +13,7 @@ import { cn } from '@/shared/shadcn/lib/utils'; import { FieldControl, type ControlledChildProps } from './field-control'; type Direction = 'horizontal' | 'vertical'; -type Scale = 'small' | 'medium'; +type Size = 'small' | 'medium'; const TYPO = { small: { @@ -43,7 +43,7 @@ export interface FormFieldProps< description?: React.ReactNode; required?: boolean; direction?: Direction; - scale?: Scale; + size?: Size; id?: string; showCounterRight?: boolean; @@ -64,7 +64,7 @@ export default function FormField< description, required = false, direction = 'horizontal', - scale = 'small', + size = 'small', id, children, showCounterRight = false, @@ -97,7 +97,7 @@ export default function FormField<
@@ -108,7 +108,7 @@ export default function FormField<
{helper && ( -
+
{helper}
)} @@ -126,7 +126,7 @@ export default function FormField<
{showCounterRight && typeof counterMax === 'number' && ( -
+
{currentLen}/{counterMax}
)} From dec9942dbbc07d5ef844a45b38de018ab34eae88 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Fri, 3 Oct 2025 03:35:36 +0900 Subject: [PATCH 16/20] =?UTF-8?q?style:=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/my-page/ui/profile-info-edit-modal.tsx | 4 ++-- src/features/study/participation/ui/start-study-modal.tsx | 4 ++-- src/shared/ui/toggle/group.tsx | 8 ++++---- src/shared/ui/toggle/index.tsx | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/features/my-page/ui/profile-info-edit-modal.tsx b/src/features/my-page/ui/profile-info-edit-modal.tsx index e3e856b5..254052fa 100644 --- a/src/features/my-page/ui/profile-info-edit-modal.tsx +++ b/src/features/my-page/ui/profile-info-edit-modal.tsx @@ -13,7 +13,7 @@ import { MultiDropdown, SingleDropdown } from '@/shared/ui/dropdown'; import FormField from '@/shared/ui/form/form-field'; import { TextAreaInput } from '@/shared/ui/input'; import { Modal } from '@/shared/ui/modal'; -import { ToggleGroup } from '@/shared/ui/toggle'; +import { GroupItems } from '@/shared/ui/toggle'; import { ProfileInfoFormSchema, @@ -210,7 +210,7 @@ function ProfileInfoEditForm({ direction="vertical" required > - + diff --git a/src/features/study/participation/ui/start-study-modal.tsx b/src/features/study/participation/ui/start-study-modal.tsx index af53ff0d..4ee469a9 100644 --- a/src/features/study/participation/ui/start-study-modal.tsx +++ b/src/features/study/participation/ui/start-study-modal.tsx @@ -26,7 +26,7 @@ import FormField from '@/shared/ui/form/form-field'; import { BaseInput, TextAreaInput } from '@/shared/ui/input'; import { Modal } from '@/shared/ui/modal'; -import { ToggleGroup } from '@/shared/ui/toggle'; +import { GroupItems } from '@/shared/ui/toggle'; interface StartStudyModalProps { memberId: number; @@ -229,7 +229,7 @@ function StartStudyForm({ direction="vertical" required > - + diff --git a/src/shared/ui/toggle/group.tsx b/src/shared/ui/toggle/group.tsx index 7df7c946..7a2c06da 100644 --- a/src/shared/ui/toggle/group.tsx +++ b/src/shared/ui/toggle/group.tsx @@ -32,7 +32,7 @@ export interface ItemRendererProps { children: ReactNode; } -function DefaultToggleItem({ pressed, onPress, children }: ItemRendererProps) { +function ToggleGroup({ pressed, onPress, children }: ItemRendererProps) { return ( {children} @@ -40,10 +40,10 @@ function DefaultToggleItem({ pressed, onPress, children }: ItemRendererProps) { ); } -function ToggleGroup(props: ToggleGroupProps) { +function GroupItems(props: ToggleGroupProps) { const { options } = props; const isMulti = props.multiple !== false; - const Item = props.renderItem ?? DefaultToggleItem; + const Item = props.renderItem ?? ToggleGroup; const selectedSet = new Set( isMulti ? (props.value ?? []) : props.value ? [props.value] : [], @@ -80,4 +80,4 @@ function ToggleGroup(props: ToggleGroupProps) { ); } -export default ToggleGroup; +export default GroupItems; diff --git a/src/shared/ui/toggle/index.tsx b/src/shared/ui/toggle/index.tsx index 8d9c83e8..e3372b09 100644 --- a/src/shared/ui/toggle/index.tsx +++ b/src/shared/ui/toggle/index.tsx @@ -1,3 +1,3 @@ export * from './switch'; export { default as ToggleButton } from './button'; -export { default as ToggleGroup } from './group'; +export { default as GroupItems } from './group'; From da6e68d4b78e56136edeee9558ad7130d4e88295 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Fri, 3 Oct 2025 03:36:01 +0900 Subject: [PATCH 17/20] =?UTF-8?q?feat:=20step1=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/ui/step/step1-group.tsx | 225 +++++++++++++----- 1 file changed, 170 insertions(+), 55 deletions(-) diff --git a/src/features/study/group/ui/step/step1-group.tsx b/src/features/study/group/ui/step/step1-group.tsx index 7f2b2a38..8792c5de 100644 --- a/src/features/study/group/ui/step/step1-group.tsx +++ b/src/features/study/group/ui/step/step1-group.tsx @@ -1,15 +1,21 @@ 'use client'; -import { useState } from 'react'; +import { + Controller, + useController, + useFormContext, + useWatch, +} from 'react-hook-form'; import { SingleDropdown } from '@/shared/ui/dropdown'; import FormField from '@/shared/ui/form/form-field'; import { BaseInput } from '@/shared/ui/input'; -import { ToggleGroup } from '@/shared/ui/toggle'; +import { RadioGroup, RadioGroupItem } from '@/shared/ui/radio'; +import { GroupItems } from '@/shared/ui/toggle'; import { EXPERIENCE_LEVEL_OPTIONS, + STUDY_TYPES, + MEETING_OPTIONS, METHOD_OPTIONS, - REGULAR_MEETING_OPTIONS, - TYPE_OPTIONS, } from '../../const/group-const'; import { OpenGroupFormValues } from '../../model/open-group-form.schema'; @@ -20,19 +26,33 @@ const ROLE_OPTIONS = [ { label: 'UX/UI 디자이너', value: 'uxui-designer' }, ]; -const typeOptions = TYPE_OPTIONS.map((v) => ({ label: v, value: v })); const expOptions = EXPERIENCE_LEVEL_OPTIONS.map((v) => ({ label: v, value: v, })); + const methodOptions = METHOD_OPTIONS.map((v) => ({ label: v, value: v })); -const meetingOptions = REGULAR_MEETING_OPTIONS.map((v) => ({ - label: v, - value: v, -})); + +const memberOptions = Array.from({ length: 20 }, (_, i) => { + const value = (i + 1).toString(); + + return { label: `${value}명`, value }; +}); export default function Step1OpenGroupStudy() { - const [method, setMethod] = useState(undefined); + const { control, formState } = useFormContext(); + const { field: typeField } = useController({ + name: 'type', + control, + }); + const { field: regularMeetingField } = useController({ + name: 'regularMeeting', + control, + }); + const methodValue = useWatch({ + control, + name: 'method', + }); return ( <> @@ -45,10 +65,24 @@ export default function Step1OpenGroupStudy() { size="medium" required > - {/* todo: 여기 radio 변경 */} - + + {STUDY_TYPES.map((type, index) => ( +
+ + +
+ ))} +
- name="targetRole" label="모집 대상" @@ -57,51 +91,75 @@ export default function Step1OpenGroupStudy() { size="medium" required > - + - - - name="maxMembers" + + name="maxMembersCount" label="모집 인원" - // todo: 여기 description 변경되면 수정 필요 - helper="모집 인원을 입력해 주세요." + helper="모집할 최대 참여 인원을 선택해주세요." direction="vertical" size="medium" required > - + - - - name="experienceLevel" + + name="experienceLevels" label="경력 여부" - helper="함께할 구성원의 경력 레벨을 선택해 주세요." + helper="스터디 참여에 필요한 경력 조건을 선택해주세요.(복수 선택 가능)" direction="vertical" size="medium" required > - + +
+
+
+ +
필수
+
+
+ 스터디가 진행되는 방식을 선택해주세요. +
- - name="method" - label="진행 방식" - helper="스터디가 진행되는 방식을 선택해주세요. (예: 온라인, 오프라인 등)" - direction="vertical" - size="medium" - required - > -
- - -
- +
+ ( + + )} + /> + ( + + )} + /> +
+ {(formState.errors.method || formState.errors.location) && ( +
+ {formState.errors.method?.message || + formState.errors.location?.message} +
+ )} +
+
name="regularMeeting" label="정기 모임" @@ -110,24 +168,81 @@ export default function Step1OpenGroupStudy() { size="medium" required > - {/* todo: 여기 radio 변경 */} - + + {MEETING_OPTIONS.map((type, index) => ( +
+ + +
+ ))} +
+
+
+
+ +
필수
+
+
+ 스터디 진행 시작일과 종료일을 선택해주세요. +
+ +
+ ( + + )} + /> +
~
+ ( + + )} + /> +
- - name="startDate" - label="진행 기간" - helper="스터디가 운영될 기간을 입력해주세요." + {(formState.errors.startDate || formState.errors.endDate) && ( +
+ {formState.errors.startDate?.message || + formState.errors.endDate?.message} +
+ )} +
+
+ {/* API에는 있는데 디자인에는 없음. 뭐지??? */} + {/* + name="price" + label="참가비" + helper="참가비가 있다면 입력해주세요. (0원 가능)" direction="vertical" size="medium" required > -
- -
~
- -
- + + */} ); } From f17bc717d36e4cb764b7e63c3fc9caae42897100 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Fri, 3 Oct 2025 03:36:20 +0900 Subject: [PATCH 18/20] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A4=20api=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/group/api/group-types.ts | 64 ++++++--- src/features/study/group/const/group-const.ts | 19 +-- .../group/model/open-group-form.schema.ts | 122 ++++++++++++++---- 3 files changed, 154 insertions(+), 51 deletions(-) diff --git a/src/features/study/group/api/group-types.ts b/src/features/study/group/api/group-types.ts index df93bd1f..070f01e2 100644 --- a/src/features/study/group/api/group-types.ts +++ b/src/features/study/group/api/group-types.ts @@ -1,18 +1,52 @@ -import { - ExperienceLevelOption, - MethodOption, - RegularMeetingOption, - TypeOption, -} from '../const/group-const'; - -export interface OpenGroupRequest { - type: TypeOption; - targetRole: string; - maxMembers: number; - experienceLevel: ExperienceLevelOption; - method: MethodOption; - regularMeeting: RegularMeetingOption; +export interface BasicInfo { + groupStudyId: number; + type: string; + targetRoles: string[]; + maxMembersCount: number; + experienceLevels: string[]; + method: 'ONLINE' | 'OFFLINE' | 'HYBRID'; + regularMeeting: 'NONE' | 'WEEKLY' | 'BIWEEKLY' | 'TRIPLE_WEEKLY_OR_MORE'; + location: string; startDate: string; - durationWeeks: number; + endDate: string; price: number; + status: 'RECRUITING' | 'IN_PROGRESS' | 'COMPLETED'; + createdAt: string; + updatedAt: string; +} + +export interface DetailInfo { + thumbnail: { + imageId: number; + resizedImages: { + resizedImageId: number; + resizedImageUrl: string; + imageSizeType: { + imageTypeName: string; + width: number | undefined; + height: number | undefined; + }; + }[]; + }; + title: string; + description: string; + summary: string; +} + +export interface InterviewPost { + interviewPost: string; +} + +export interface OpenGroupRequest { + basicInfo: BasicInfo; + detailInfo: DetailInfo; + interviewPost: InterviewPost; + thumbnailExtension: + | 'DEFAULT' + | 'JPG' + | 'PNG' + | 'GIF' + | 'WEBP' + | 'SVG' + | 'JPEG'; } diff --git a/src/features/study/group/const/group-const.ts b/src/features/study/group/const/group-const.ts index 6688b217..0cd7a48f 100644 --- a/src/features/study/group/const/group-const.ts +++ b/src/features/study/group/const/group-const.ts @@ -1,10 +1,10 @@ -export const TYPE_OPTIONS = [ +export const STUDY_TYPES = [ '프로젝트', '멘토링', '세미나', '챌린지', - '책스터디', - '강의스터디', + '책 스터디', + '강의 스터디', ] as const; export const EXPERIENCE_LEVEL_OPTIONS = [ @@ -18,14 +18,9 @@ export const EXPERIENCE_LEVEL_OPTIONS = [ export const METHOD_OPTIONS = ['온라인', '오프라인', '온오프라인'] as const; -export const REGULAR_MEETING_OPTIONS = [ +export const MEETING_OPTIONS = [ '없음', - '주1회', - '주2회', - '주3회이상', + '주 1회', + '주 2회', + '주 3회 이상', ] as const; - -export type TypeOption = (typeof TYPE_OPTIONS)[number]; -export type ExperienceLevelOption = (typeof EXPERIENCE_LEVEL_OPTIONS)[number]; -export type MethodOption = (typeof METHOD_OPTIONS)[number]; -export type RegularMeetingOption = (typeof REGULAR_MEETING_OPTIONS)[number]; diff --git a/src/features/study/group/model/open-group-form.schema.ts b/src/features/study/group/model/open-group-form.schema.ts index 9afc32d1..05f36b8c 100644 --- a/src/features/study/group/model/open-group-form.schema.ts +++ b/src/features/study/group/model/open-group-form.schema.ts @@ -1,33 +1,67 @@ import { z } from 'zod'; import { OpenGroupRequest } from '../api/group-types'; import { - EXPERIENCE_LEVEL_OPTIONS, METHOD_OPTIONS, - REGULAR_MEETING_OPTIONS, - TYPE_OPTIONS, + MEETING_OPTIONS, + STUDY_TYPES, } from '../const/group-const'; -// YYYY-MM-DD 형식 검증 const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; -const ExperienceLevelEnum = z.enum(EXPERIENCE_LEVEL_OPTIONS); + +const StudyMap: Record< + string, + | 'PROJECT' + | 'MENTORING' + | 'SEMINAR' + | 'CHALLENGE' + | 'BOOK_STUDY' + | 'LECTURE_STUDY' +> = { + 프로젝트: 'PROJECT', + 멘토링: 'MENTORING', + 세미나: 'SEMINAR', + 챌린지: 'CHALLENGE', + '책 스터디': 'BOOK_STUDY', + '강의 스터디': 'LECTURE_STUDY', +}; + +const MethodMap: Record = { + 온라인: 'ONLINE', + 오프라인: 'OFFLINE', + 온오프라인: 'HYBRID', +}; + +const RegularMeetingMap: Record< + string, + 'WEEKLY' | 'BIWEEKLY' | 'TRIPLE_WEEKLY_OR_MORE' | 'NONE' +> = { + '주 1회': 'WEEKLY', + '주 2회': 'BIWEEKLY', + '주 3회 이상': 'TRIPLE_WEEKLY_OR_MORE', + 없음: 'NONE', +}; export const OpenGroupFormSchema = z.object({ - type: z.enum(TYPE_OPTIONS), + type: z.enum(STUDY_TYPES), targetRole: z.array(z.string()).min(1, '역할을 1개 이상 선택해 주세요.'), - maxMembers: z + maxMembersCount: z .string() .trim() .regex(/^[1-9]\d*$/, '최소 1 이상의 정수를 입력해 주세요.'), - experienceLevel: z - .union([ExperienceLevelEnum, z.literal('')]) - .transform((v) => (v === '' ? undefined : v)) - .pipe(ExperienceLevelEnum), + experienceLevels: z + .array(z.string()) + .min(1, '경력을 1개 이상 선택해 주세요.'), method: z.enum(METHOD_OPTIONS), - regularMeeting: z.enum(REGULAR_MEETING_OPTIONS), + location: z.string().trim(), + regularMeeting: z.enum(MEETING_OPTIONS), startDate: z .string() .trim() .regex(ISO_DATE_REGEX, 'YYYY-MM-DD 형식의 시작일을 입력해 주세요.'), + endDate: z + .string() + .trim() + .regex(ISO_DATE_REGEX, 'YYYY-MM-DD 형식의 시작일을 입력해 주세요.'), durationWeeks: z .string() .trim() @@ -36,6 +70,19 @@ export const OpenGroupFormSchema = z.object({ .string() .trim() .regex(/^\d+$/, '가격은 0 이상 정수로 입력해 주세요.'), + title: z.string().trim(), + description: z.string().trim(), + summary: z.string().trim(), + interviewPost: z.string().optional(), + thumbnailExtension: z.enum([ + 'DEFAULT', + 'JPG', + 'PNG', + 'GIF', + 'WEBP', + 'SVG', + 'JPEG', + ]), }); export type OpenGroupFormValues = z.input; @@ -45,26 +92,53 @@ export function buildOpenGroupDefaultValues(): OpenGroupFormValues { return { type: '프로젝트', targetRole: [], - maxMembers: '', - experienceLevel: '', + maxMembersCount: '', + experienceLevels: [], method: '온라인', - regularMeeting: '주1회', + location: '', + regularMeeting: '주 1회', startDate: '', + endDate: '', durationWeeks: '', price: '', + title: '', + description: '', + summary: '', + interviewPost: '', + thumbnailExtension: 'DEFAULT', }; } export function toOpenGroupRequest(v: OpenGroupParsedValues): OpenGroupRequest { return { - type: v.type, - targetRole: v.targetRole.join(','), - maxMembers: Number(v.maxMembers), - experienceLevel: v.experienceLevel, - method: v.method, - regularMeeting: v.regularMeeting, - startDate: v.startDate.trim(), - durationWeeks: Number(v.durationWeeks), - price: Number(v.price), + basicInfo: { + groupStudyId: 0, + type: StudyMap[v.type], + targetRoles: v.targetRole, + maxMembersCount: Number(v.maxMembersCount), + experienceLevels: v.experienceLevels ?? [], + method: MethodMap[v.method], + regularMeeting: RegularMeetingMap[v.regularMeeting], + location: v.location.trim(), + startDate: v.startDate.trim(), + endDate: v.endDate.trim(), + price: Number(v.price), + status: 'RECRUITING', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + detailInfo: { + thumbnail: { + imageId: 0, + resizedImages: [], + }, + title: '', + description: '', + summary: '', + }, + interviewPost: { + interviewPost: v.interviewPost ?? '', + }, + thumbnailExtension: v.thumbnailExtension, }; } From 80b6477af577c3c5d77f1c069e61df51eba3c6ce Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Fri, 3 Oct 2025 03:40:48 +0900 Subject: [PATCH 19/20] =?UTF-8?q?fix:=20next=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/model/open-group-form.schema.ts | 5 --- .../study/group/ui/open-group-modal.tsx | 32 +++++++++++-------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/features/study/group/model/open-group-form.schema.ts b/src/features/study/group/model/open-group-form.schema.ts index 05f36b8c..595c4640 100644 --- a/src/features/study/group/model/open-group-form.schema.ts +++ b/src/features/study/group/model/open-group-form.schema.ts @@ -62,10 +62,6 @@ export const OpenGroupFormSchema = z.object({ .string() .trim() .regex(ISO_DATE_REGEX, 'YYYY-MM-DD 형식의 시작일을 입력해 주세요.'), - durationWeeks: z - .string() - .trim() - .regex(/^[1-9]\d*$/, '기간은 1 이상 정수로 입력해 주세요.'), price: z .string() .trim() @@ -99,7 +95,6 @@ export function buildOpenGroupDefaultValues(): OpenGroupFormValues { regularMeeting: '주 1회', startDate: '', endDate: '', - durationWeeks: '', price: '', title: '', description: '', diff --git a/src/features/study/group/ui/open-group-modal.tsx b/src/features/study/group/ui/open-group-modal.tsx index f673fec7..7344d8b8 100644 --- a/src/features/study/group/ui/open-group-modal.tsx +++ b/src/features/study/group/ui/open-group-modal.tsx @@ -14,9 +14,9 @@ import { buildOpenGroupDefaultValues, toOpenGroupRequest, } from '../model/open-group-form.schema'; -import Step1GroupStudy from './step/step1-group'; -import Step2GroupStudy from './step/step2-group'; -import Step3GroupStudy from './step/step3-group'; +import Step1OpenGroupStudy from './step/step1-group'; +import Step2OpenGroupStudy from './step/step2-group'; +import Step3OpenGroupStudy from './step/step3-group'; function Stepper({ step }: { step: 1 | 2 | 3 }) { const dot = (n: 1 | 2 | 3) => { @@ -61,7 +61,7 @@ export default function OpenGroupStudyModal({ }: OpenGroupStudyModalProps) { return ( - {trigger ? {trigger} : null} + {trigger && {trigger}} @@ -90,25 +90,31 @@ function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { const [step, setStep] = useState<1 | 2 | 3>(1); - // 스텝별 검증 필드 지정 const STEP_FIELDS: Record<1 | 2 | 3, (keyof OpenGroupFormValues)[]> = { 1: [ 'type', 'targetRole', - 'maxMembers', - 'experienceLevel', + 'maxMembersCount', + 'experienceLevels', 'method', + 'location', 'regularMeeting', 'startDate', + 'endDate', + // 'price', ], - 2: ['type'], - 3: ['price'], + 2: ['thumbnailExtension'], + 3: ['interviewPost'], }; const goNext = async () => { const fields = STEP_FIELDS[step]; const ok = await trigger(fields as any, { shouldFocus: true }); - if (!ok) return; + if (!ok) { + console.log('trigger failed. errors:', methods.formState.errors); + + return; + } if (step < 3) setStep((s) => (s + 1) as 1 | 2 | 3); }; @@ -134,9 +140,9 @@ function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { className="flex flex-col gap-400" onSubmit={handleSubmit(onSubmit)} > - {step === 1 && } - {step === 2 && } - {step === 3 && } + {step === 1 && } + {step === 2 && } + {step === 3 && } From 06d3ac4b91525238e668d4da10345e487b482b1b Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Fri, 3 Oct 2025 03:41:05 +0900 Subject: [PATCH 20/20] =?UTF-8?q?style:=20=EC=A4=91=EA=B0=84=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=EC=9A=A9=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/ui/step/step2-group.tsx | 41 +++++++++++++------ .../study/group/ui/step/step3-group.tsx | 11 +++-- src/widgets/home/sidebar.tsx | 4 +- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/features/study/group/ui/step/step2-group.tsx b/src/features/study/group/ui/step/step2-group.tsx index 7959ab51..e5efa38d 100644 --- a/src/features/study/group/ui/step/step2-group.tsx +++ b/src/features/study/group/ui/step/step2-group.tsx @@ -1,28 +1,45 @@ 'use client'; +import { useState } from 'react'; import FormField from '@/shared/ui/form/form-field'; import { BaseInput, TextAreaInput } from '@/shared/ui/input'; import { OpenGroupFormValues } from '../../model/open-group-form.schema'; +import GroupStudyThumbnailInput from '../group-study-thumbnail-input'; + +const THUMBNAIL_EXTENSIONS = [ + { label: 'DEFAULT', value: 'DEFAULT' }, + { label: 'JPG', value: 'JPG' }, + { label: 'PNG', value: 'PNG' }, + { label: 'GIF', value: 'GIF' }, + { label: 'WEBP', value: 'WEBP' }, + { label: 'SVG', value: 'SVG' }, + { label: 'JPEG', value: 'JPEG' }, +]; export default function Step2OpenGroupStudy() { - // todo: 그룹 폼 필드 변경해야 함 + const [image, setImage] = useState(undefined); + const [thumbnailExt, setThumbnailExt] = useState( + undefined, + ); + return ( <>
스터디 소개 작성
- - name="type" + + + name="thumbnailExtension" label="썸네일" direction="vertical" size="medium" required > - {/* todo: 여기 파일 업로드 변경 */} - + - - name="type" + + + name="title" label="스터디 제목" direction="vertical" size="medium" @@ -31,8 +48,8 @@ export default function Step2OpenGroupStudy() { - - name="type" + + name="summary" label="스터디 한 줄 소개" direction="vertical" size="medium" @@ -41,14 +58,14 @@ export default function Step2OpenGroupStudy() { - - name="type" + + name="description" label="스터디 소개" direction="vertical" size="medium" required > - + ); diff --git a/src/features/study/group/ui/step/step3-group.tsx b/src/features/study/group/ui/step/step3-group.tsx index 7101d523..d639bda1 100644 --- a/src/features/study/group/ui/step/step3-group.tsx +++ b/src/features/study/group/ui/step/step3-group.tsx @@ -11,15 +11,14 @@ export default function Step3OpenGroupStudy() {
지원 & 규칙 설정
- - name="targetRole" - label="스터디원에게 보여줄 질문을 입력하세요" - helper="스터디 지원자가 신청 시 작성해야 할 질문을 설정하세요. (예: 지원 동기, 경험, 기대하는 점 등)" + + name="interviewPost" + label="스터디 지원 시 작성할 질문" + helper="스터디 지원자가 신청할 때 답변해야 하는 질문들을 작성하세요. (예: 지원 동기, 관련 경험, 기대하는 점 등)" direction="vertical" size="medium" - required > - +
diff --git a/src/widgets/home/sidebar.tsx b/src/widgets/home/sidebar.tsx index c96352de..dc525acd 100644 --- a/src/widgets/home/sidebar.tsx +++ b/src/widgets/home/sidebar.tsx @@ -61,7 +61,7 @@ export default async function Sidebar() { /> )} {/* todo: 편하게 확인하시라고 추가했습니다. 이후 목록 추가되고 연결되면 제가 지워서 커밋 올리도록 하겠습니다. */} - {/* @@ -75,7 +75,7 @@ export default async function Sidebar() {

} - /> */} + />