From 3965a01ef75440c624e124c2e56cc05f67004a43 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sun, 28 Sep 2025 01:33:07 +0900 Subject: [PATCH 01/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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() {

} - /> */} + /> Date: Thu, 9 Oct 2025 20:05:48 +0900 Subject: [PATCH 21/31] =?UTF-8?q?fix:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=97=90=20=EB=A7=9E=EA=B2=8C=20step1=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 --- .../study/group/api/group-study-types.ts | 48 +++++++++ src/features/study/group/api/group-types.ts | 52 --------- src/features/study/group/const/group-const.ts | 26 ----- .../study/group/const/group-study-const.ts | 81 ++++++++++++++ .../group/model/open-group-form.schema.ts | 101 ++++++------------ .../study/group/ui/open-group-modal.tsx | 2 +- .../study/group/ui/step/step1-group.tsx | 42 ++++---- 7 files changed, 180 insertions(+), 172 deletions(-) create mode 100644 src/features/study/group/api/group-study-types.ts delete mode 100644 src/features/study/group/api/group-types.ts delete mode 100644 src/features/study/group/const/group-const.ts create mode 100644 src/features/study/group/const/group-study-const.ts diff --git a/src/features/study/group/api/group-study-types.ts b/src/features/study/group/api/group-study-types.ts new file mode 100644 index 00000000..196a236b --- /dev/null +++ b/src/features/study/group/api/group-study-types.ts @@ -0,0 +1,48 @@ +import { + EXPERIENCE_LEVEL_OPTIONS, + REGULAR_MEETINGS, + STUDY_METHODS, + STUDY_TYPES, + TARGET_ROLE_OPTIONS, + THUMBNAIL_EXTENSION, +} from '../const/group-study-const'; + +export type StudyType = (typeof STUDY_TYPES)[number]; +export type TargetRole = (typeof TARGET_ROLE_OPTIONS)[number]; +export type ExperienceLevel = (typeof EXPERIENCE_LEVEL_OPTIONS)[number]; +export type StudyMethod = (typeof STUDY_METHODS)[number]; +export type RegularMeeting = (typeof REGULAR_MEETINGS)[number]; +export type ThumbnailExtension = (typeof THUMBNAIL_EXTENSION)[number]; + +export interface BasicInfo { + type: StudyType; + targetRoles: TargetRole[]; + maxMembersCount: number; + experienceLevels: ExperienceLevel[]; + method: StudyMethod; + regularMeeting: RegularMeeting; + location: string; + startDate: string; + endDate: string; + price: number; + createdAt: string; + updatedAt: string; +} + +export interface DetailInfo { + title: string; + description: string; + summary: string; + thumbnailExtension: ThumbnailExtension; +} + +export interface InterviewPost { + interviewPost: string[]; +} + +export interface OpenGroupStudyRequest { + basicInfo: BasicInfo; + detailInfo: DetailInfo; + interviewPost: InterviewPost; + thumbnailExtension: ThumbnailExtension; +} diff --git a/src/features/study/group/api/group-types.ts b/src/features/study/group/api/group-types.ts deleted file mode 100644 index 070f01e2..00000000 --- a/src/features/study/group/api/group-types.ts +++ /dev/null @@ -1,52 +0,0 @@ -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; - 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 deleted file mode 100644 index 0cd7a48f..00000000 --- a/src/features/study/group/const/group-const.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const STUDY_TYPES = [ - '프로젝트', - '멘토링', - '세미나', - '챌린지', - '책 스터디', - '강의 스터디', -] as const; - -export const EXPERIENCE_LEVEL_OPTIONS = [ - '입문자', - '취준생', - '주니어 (1~3년차)', - '중니어 (4~6년차)', - '시니어 (7년차 이상)', - '경력 무관', -] as const; - -export const METHOD_OPTIONS = ['온라인', '오프라인', '온오프라인'] as const; - -export const MEETING_OPTIONS = [ - '없음', - '주 1회', - '주 2회', - '주 3회 이상', -] as const; diff --git a/src/features/study/group/const/group-study-const.ts b/src/features/study/group/const/group-study-const.ts new file mode 100644 index 00000000..3de1f4c4 --- /dev/null +++ b/src/features/study/group/const/group-study-const.ts @@ -0,0 +1,81 @@ +export const STUDY_TYPES = [ + 'PROJECT', + 'MENTORING', + 'SEMINAR', + 'CHALLENGE', + 'BOOK_STUDY', + 'LECTURE_STUDY', +] as const; + +export const TARGET_ROLE_OPTIONS = [ + 'BACKEND', + 'FRONTEND', + 'PLANNER', + 'DESIGNER', +] as const; + +export const EXPERIENCE_LEVEL_OPTIONS = [ + 'BEGINNER', + 'JOB_SEEKER', + 'JUNIOR', + 'MIDDLE', + 'SENIOR', +] as const; + +export const STUDY_METHODS = ['ONLINE', 'OFFLINE', 'HYBRID'] as const; + +export const REGULAR_MEETINGS = [ + 'NONE', + 'WEEKLY', + 'BIWEEKLY', + 'TRIPLE_WEEKLY_OR_MORE', +] as const; + +export const THUMBNAIL_EXTENSION = [ + 'DEFAULT', + 'JPG', + 'PNG', + 'GIF', + 'WEBP', + 'SVG', + 'JPEG', +] as const; + +// UI 매칭을 위한 라벨 const + +export const STUDY_TYPE_LABELS = { + PROJECT: '프로젝트', + MENTORING: '멘토링', + SEMINAR: '세미나', + CHALLENGE: '챌린지', + BOOK_STUDY: '책 스터디', + LECTURE_STUDY: '강의 스터디', +} as const; + +export const ROLE_OPTIONS_UI = [ + { label: '백엔드', value: 'BACKEND' }, + { label: '프론트엔드', value: 'FRONTEND' }, + { label: '기획', value: 'PLANNER' }, + { label: '디자이너', value: 'DESIGNER' }, +]; + +export const EXPERIENCE_LEVEL_OPTIONS_UI = [ + { label: '입문자', value: 'BEGINNER' }, + { label: '취준생', value: 'JOB_SEEKER' }, + { label: '주니어', value: 'JUNIOR' }, + { label: '미들', value: 'MIDDLE' }, + { label: '시니어', value: 'SENIOR' }, +]; + +export const STUDY_METHOD_LABELS = { + ONLINE: '온라인', + OFFLINE: '오프라인', + HYBRID: '온오프라인', +} as const; + +export const REGULAR_MEETING_LABELS = { + NONE: '없음', + WEEKLY: '주 1회', + BIWEEKLY: '주 2회', + TRIPLE_WEEKLY_OR_MORE: '주 3회 이상', +} as const; 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 595c4640..a5c7c438 100644 --- a/src/features/study/group/model/open-group-form.schema.ts +++ b/src/features/study/group/model/open-group-form.schema.ts @@ -1,59 +1,31 @@ import { z } from 'zod'; -import { OpenGroupRequest } from '../api/group-types'; +import { OpenGroupStudyRequest } from '../api/group-study-types'; import { - METHOD_OPTIONS, - MEETING_OPTIONS, STUDY_TYPES, -} from '../const/group-const'; + TARGET_ROLE_OPTIONS, + EXPERIENCE_LEVEL_OPTIONS, + REGULAR_MEETINGS, + THUMBNAIL_EXTENSION, + STUDY_METHODS, +} from '../const/group-study-const'; const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; -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(STUDY_TYPES), - targetRole: z.array(z.string()).min(1, '역할을 1개 이상 선택해 주세요.'), + targetRoles: z + .array(z.enum(TARGET_ROLE_OPTIONS)) + .min(1, '역할을 1개 이상 선택해 주세요.'), maxMembersCount: z .string() .trim() .regex(/^[1-9]\d*$/, '최소 1 이상의 정수를 입력해 주세요.'), experienceLevels: z - .array(z.string()) + .array(z.enum(EXPERIENCE_LEVEL_OPTIONS)) .min(1, '경력을 1개 이상 선택해 주세요.'), - method: z.enum(METHOD_OPTIONS), + method: z.enum(STUDY_METHODS), location: z.string().trim(), - regularMeeting: z.enum(MEETING_OPTIONS), + regularMeeting: z.enum(REGULAR_MEETINGS), startDate: z .string() .trim() @@ -61,7 +33,7 @@ export const OpenGroupFormSchema = z.object({ endDate: z .string() .trim() - .regex(ISO_DATE_REGEX, 'YYYY-MM-DD 형식의 시작일을 입력해 주세요.'), + .regex(ISO_DATE_REGEX, 'YYYY-MM-DD 형식의 종료일을 입력해 주세요.'), price: z .string() .trim() @@ -70,15 +42,7 @@ export const OpenGroupFormSchema = z.object({ description: z.string().trim(), summary: z.string().trim(), interviewPost: z.string().optional(), - thumbnailExtension: z.enum([ - 'DEFAULT', - 'JPG', - 'PNG', - 'GIF', - 'WEBP', - 'SVG', - 'JPEG', - ]), + thumbnailExtension: z.enum(THUMBNAIL_EXTENSION), }); export type OpenGroupFormValues = z.input; @@ -86,13 +50,13 @@ export type OpenGroupParsedValues = z.output; export function buildOpenGroupDefaultValues(): OpenGroupFormValues { return { - type: '프로젝트', - targetRole: [], + type: 'PROJECT', + targetRoles: [], maxMembersCount: '', experienceLevels: [], - method: '온라인', + method: 'ONLINE', location: '', - regularMeeting: '주 1회', + regularMeeting: 'NONE', startDate: '', endDate: '', price: '', @@ -104,35 +68,32 @@ export function buildOpenGroupDefaultValues(): OpenGroupFormValues { }; } -export function toOpenGroupRequest(v: OpenGroupParsedValues): OpenGroupRequest { +export function toOpenGroupRequest( + v: OpenGroupParsedValues, +): OpenGroupStudyRequest { return { basicInfo: { - groupStudyId: 0, - type: StudyMap[v.type], - targetRoles: v.targetRole, + type: v.type, + targetRoles: v.targetRoles, maxMembersCount: Number(v.maxMembersCount), experienceLevels: v.experienceLevels ?? [], - method: MethodMap[v.method], - regularMeeting: RegularMeetingMap[v.regularMeeting], + method: v.method, + regularMeeting: 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: '', + thumbnailExtension: v.thumbnailExtension, + title: v.title, + description: v.description, + summary: v.summary, }, interviewPost: { - interviewPost: v.interviewPost ?? '', + interviewPost: v.interviewPost ? [v.interviewPost] : [], }, thumbnailExtension: v.thumbnailExtension, }; diff --git a/src/features/study/group/ui/open-group-modal.tsx b/src/features/study/group/ui/open-group-modal.tsx index 7344d8b8..f1d5b9bb 100644 --- a/src/features/study/group/ui/open-group-modal.tsx +++ b/src/features/study/group/ui/open-group-modal.tsx @@ -93,7 +93,7 @@ function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { const STEP_FIELDS: Record<1 | 2 | 3, (keyof OpenGroupFormValues)[]> = { 1: [ 'type', - 'targetRole', + 'targetRoles', 'maxMembersCount', 'experienceLevels', 'method', diff --git a/src/features/study/group/ui/step/step1-group.tsx b/src/features/study/group/ui/step/step1-group.tsx index 8792c5de..e519e02f 100644 --- a/src/features/study/group/ui/step/step1-group.tsx +++ b/src/features/study/group/ui/step/step1-group.tsx @@ -11,28 +11,24 @@ import FormField from '@/shared/ui/form/form-field'; import { BaseInput } from '@/shared/ui/input'; import { RadioGroup, RadioGroupItem } from '@/shared/ui/radio'; import { GroupItems } from '@/shared/ui/toggle'; +import { TargetRole } from '../../api/group-study-types'; import { - EXPERIENCE_LEVEL_OPTIONS, STUDY_TYPES, - MEETING_OPTIONS, - METHOD_OPTIONS, -} from '../../const/group-const'; + ROLE_OPTIONS_UI, + EXPERIENCE_LEVEL_OPTIONS_UI, + STUDY_METHODS, + STUDY_METHOD_LABELS, + STUDY_TYPE_LABELS, + REGULAR_MEETINGS, + REGULAR_MEETING_LABELS, +} from '../../const/group-study-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 expOptions = EXPERIENCE_LEVEL_OPTIONS.map((v) => ({ - label: v, +const methodOptions = STUDY_METHODS.map((v) => ({ + label: STUDY_METHOD_LABELS[v], value: v, })); -const methodOptions = METHOD_OPTIONS.map((v) => ({ label: v, value: v })); - const memberOptions = Array.from({ length: 20 }, (_, i) => { const value = (i + 1).toString(); @@ -77,21 +73,21 @@ export default function Step1OpenGroupStudy() { htmlFor={`option${index}`} className="font-designer-14m text-text-default" > - {type} + {STUDY_TYPE_LABELS[type]}
))} - - name="targetRole" + + name="targetRoles" label="모집 대상" helper="함께하고 싶은 대상(직무·관심사 등)을 선택해주세요. (복수 선택 가능)" direction="vertical" size="medium" required > - + name="maxMembersCount" @@ -111,7 +107,7 @@ export default function Step1OpenGroupStudy() { size="medium" required > - +
@@ -146,7 +142,7 @@ export default function Step1OpenGroupStudy() { value={field.value} onChange={field.onChange} placeholder="위치를 입력하세요." - disabled={!methodValue || methodValue === '온라인'} + disabled={!methodValue || methodValue === 'ONLINE'} /> )} /> @@ -173,14 +169,14 @@ export default function Step1OpenGroupStudy() { value={regularMeetingField.value} onValueChange={regularMeetingField.onChange} > - {MEETING_OPTIONS.map((type, index) => ( + {REGULAR_MEETINGS.map((type, index) => (
))} From af4764d57e4e569129410ebbda3d75868b20a7b0 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Thu, 9 Oct 2025 21:53:12 +0900 Subject: [PATCH 22/31] =?UTF-8?q?feat:=20step2=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=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 | 15 +++-- .../group/ui/group-study-thumbnail-input.tsx | 14 ++--- .../study/group/ui/open-group-modal.tsx | 4 +- .../study/group/ui/step/step2-group.tsx | 59 ++++++++++++++----- src/widgets/home/sidebar.tsx | 1 - 5 files changed, 60 insertions(+), 33 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 a5c7c438..c44042c0 100644 --- a/src/features/study/group/model/open-group-form.schema.ts +++ b/src/features/study/group/model/open-group-form.schema.ts @@ -38,14 +38,19 @@ export const OpenGroupFormSchema = z.object({ .string() .trim() .regex(/^\d+$/, '가격은 0 이상 정수로 입력해 주세요.'), - title: z.string().trim(), - description: z.string().trim(), - summary: z.string().trim(), + title: z.string().trim().min(1, '스터디 제목을 입력해주세요.'), + summary: z.string().trim().min(1, '한 줄 소개를 입력해주세요.'), + description: z.string().trim().min(1, '스터디 소개를 입력해주세요.'), interviewPost: z.string().optional(), - thumbnailExtension: z.enum(THUMBNAIL_EXTENSION), + thumbnailExtension: z + .enum(THUMBNAIL_EXTENSION) + .refine((val) => val !== 'DEFAULT', '썸네일 이미지를 선택해주세요.'), }); -export type OpenGroupFormValues = z.input; +// 사진 상태 저장을 위한 로컬용 state +export type OpenGroupFormValues = z.input & { + thumbnailFile?: File | undefined; +}; export type OpenGroupParsedValues = z.output; export function buildOpenGroupDefaultValues(): OpenGroupFormValues { diff --git a/src/features/study/group/ui/group-study-thumbnail-input.tsx b/src/features/study/group/ui/group-study-thumbnail-input.tsx index 319193e2..f0a3c53e 100644 --- a/src/features/study/group/ui/group-study-thumbnail-input.tsx +++ b/src/features/study/group/ui/group-study-thumbnail-input.tsx @@ -15,7 +15,7 @@ export default function GroupStudyThumbnailInput({ onChangeImage, }: { image?: string; - onChangeImage: (image?: string) => void; + onChangeImage: (file: File | null) => void; }) { const fileInputRef = useRef(null); @@ -35,7 +35,6 @@ export default function GroupStudyThumbnailInput({ const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - console.log('drag leave'); setIsDragging(false); }; // 영역 안에서 드래그 중일 때 @@ -43,8 +42,6 @@ export default function GroupStudyThumbnailInput({ e.preventDefault(); e.stopPropagation(); - console.log('drag over'); - if (e.dataTransfer.files) { setIsDragging(true); } @@ -53,13 +50,12 @@ export default function GroupStudyThumbnailInput({ const handleDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); - console.log('drop'); setIsDragging(false); const file = e.dataTransfer.files[0]; // 1장만 허용 if (file && file.type.startsWith('image/')) { - onChangeImage(URL.createObjectURL(file)); + onChangeImage(file); } }; @@ -68,7 +64,7 @@ export default function GroupStudyThumbnailInput({ const file = e.target.files?.[0]; if (file && file.type.startsWith('image/')) { - onChangeImage(URL.createObjectURL(file)); + onChangeImage(file); } }; @@ -118,8 +114,8 @@ export default function GroupStudyThumbnailInput({ preview +
From f06f47a55213501215ecda1f6820be7234a03cc0 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Fri, 10 Oct 2025 21:44:24 +0900 Subject: [PATCH 24/31] =?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=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/api/creat-group-study.ts | 9 +++++ .../group/const/use-group-study-mutation.ts | 10 +++++ .../study/group/ui/open-group-modal.tsx | 37 +++++++++++-------- .../study/group/ui/step/step1-group.tsx | 2 +- 4 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 src/features/study/group/api/creat-group-study.ts create mode 100644 src/features/study/group/const/use-group-study-mutation.ts diff --git a/src/features/study/group/api/creat-group-study.ts b/src/features/study/group/api/creat-group-study.ts new file mode 100644 index 00000000..0f4d4a65 --- /dev/null +++ b/src/features/study/group/api/creat-group-study.ts @@ -0,0 +1,9 @@ +import { axiosInstance } from '@/shared/tanstack-query/axios'; +import { OpenGroupStudyRequest } from './group-study-types'; + +// CS 스터디 매칭 신청 +export const createGroupStudy = async (payload: OpenGroupStudyRequest) => { + const res = await axiosInstance.post('/group-studies', payload); + + return res.data; +}; diff --git a/src/features/study/group/const/use-group-study-mutation.ts b/src/features/study/group/const/use-group-study-mutation.ts new file mode 100644 index 00000000..bee72b30 --- /dev/null +++ b/src/features/study/group/const/use-group-study-mutation.ts @@ -0,0 +1,10 @@ +import { useMutation } from '@tanstack/react-query'; +import { createGroupStudy } from '../api/creat-group-study'; +import { OpenGroupStudyRequest } from '../api/group-study-types'; + +// 그룹 스터디 개설 mutation +export const useCreateGroupStudyMutation = () => { + return useMutation({ + mutationFn: (payload: OpenGroupStudyRequest) => createGroupStudy(payload), + }); +}; diff --git a/src/features/study/group/ui/open-group-modal.tsx b/src/features/study/group/ui/open-group-modal.tsx index c7cb221c..ad9fe9db 100644 --- a/src/features/study/group/ui/open-group-modal.tsx +++ b/src/features/study/group/ui/open-group-modal.tsx @@ -3,7 +3,7 @@ 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 { FormProvider, useForm } from 'react-hook-form'; import Button from '@/shared/ui/button'; import { Modal } from '@/shared/ui/modal'; @@ -17,6 +17,7 @@ import { import Step1OpenGroupStudy from './step/step1-group'; import Step2OpenGroupStudy from './step/step2-group'; import Step3OpenGroupStudy from './step/step3-group'; +import { useCreateGroupStudyMutation } from '../const/use-group-study-mutation'; function Stepper({ step }: { step: 1 | 2 | 3 }) { const dot = (n: 1 | 2 | 3) => { @@ -79,6 +80,8 @@ export default function OpenGroupStudyModal({ } function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { + const { mutate: createGroupStudy } = useCreateGroupStudyMutation(); + const methods = useForm({ resolver: zodResolver(OpenGroupFormSchema), mode: 'onChange', @@ -99,7 +102,6 @@ function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { 'regularMeeting', 'startDate', 'endDate', - // 'price', ], 2: ['thumbnailExtension', 'title', 'description', 'summary'], 3: ['interviewPost'], @@ -113,6 +115,7 @@ function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { return; } + if (step < 3) setStep((s) => (s + 1) as 1 | 2 | 3); }; @@ -120,10 +123,18 @@ function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { 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('유효성 통과! (콘솔에서 요청 페이로드 미리보기 확인)'); + const onValidSubmit = (values: OpenGroupFormValues) => { + const body = toOpenGroupRequest(values); + + createGroupStudy(body, { + onSuccess: () => { + alert('그룹 스터디 개설이 완료되었습니다!'); + onClose(); + }, + onError: () => { + alert('그룹 스터디 개설 중 오류가 발생했습니다. 다시 시도해 주세요.'); + }, + }); onClose(); }; @@ -131,12 +142,11 @@ function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { <> - - {...methods}> +
{step === 1 && } {step === 2 && } @@ -165,14 +175,9 @@ function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { 취소 + {step < 3 ? ( - ) : ( diff --git a/src/features/study/group/ui/step/step1-group.tsx b/src/features/study/group/ui/step/step1-group.tsx index e519e02f..a559baaa 100644 --- a/src/features/study/group/ui/step/step1-group.tsx +++ b/src/features/study/group/ui/step/step1-group.tsx @@ -46,8 +46,8 @@ export default function Step1OpenGroupStudy() { control, }); const methodValue = useWatch({ - control, name: 'method', + control, }); return ( From 3799f5a3cbb31ce185bcde4a9d5b93033bb14634 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Fri, 10 Oct 2025 22:55:50 +0900 Subject: [PATCH 25/31] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=85=20=EB=AF=B8?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stories/ui/group-study-thumbnail-input.stories.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stories/ui/group-study-thumbnail-input.stories.tsx b/src/stories/ui/group-study-thumbnail-input.stories.tsx index 948fd35d..08422ac7 100644 --- a/src/stories/ui/group-study-thumbnail-input.stories.tsx +++ b/src/stories/ui/group-study-thumbnail-input.stories.tsx @@ -22,8 +22,9 @@ type Story = StoryObj; export const Default: Story = { render: () => { - const [image, setImage] = useState(undefined); + const [file, setFile] = useState(null); + const image = file ? URL.createObjectURL(file) : undefined; - return ; + return ; }, }; From 4edcf5952a3f0e3f1d19f47d93c106f7e4ed0da1 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Fri, 10 Oct 2025 22:57:26 +0900 Subject: [PATCH 26/31] =?UTF-8?q?style:=20=EC=A3=BC=EC=84=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/home/sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets/home/sidebar.tsx b/src/widgets/home/sidebar.tsx index f85ed5c3..044ffb5a 100644 --- a/src/widgets/home/sidebar.tsx +++ b/src/widgets/home/sidebar.tsx @@ -61,7 +61,7 @@ export default async function Sidebar() { /> )} {/* todo: 편하게 확인하시라고 추가했습니다. 이후 목록 추가되고 연결되면 제가 지워서 커밋 올리도록 하겠습니다. */} -

@@ -74,7 +74,7 @@ export default async function Sidebar() {

} - /> + /> */} Date: Sat, 11 Oct 2025 02:18:18 +0900 Subject: [PATCH 27/31] =?UTF-8?q?feat:=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81=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 --- .../study/group/api/group-study-types.ts | 12 ++++++ .../study/group/ui/open-group-modal.tsx | 40 ++++++++++++------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/features/study/group/api/group-study-types.ts b/src/features/study/group/api/group-study-types.ts index 196a236b..5f0c204e 100644 --- a/src/features/study/group/api/group-study-types.ts +++ b/src/features/study/group/api/group-study-types.ts @@ -13,6 +13,18 @@ export type ExperienceLevel = (typeof EXPERIENCE_LEVEL_OPTIONS)[number]; export type StudyMethod = (typeof STUDY_METHODS)[number]; export type RegularMeeting = (typeof REGULAR_MEETINGS)[number]; export type ThumbnailExtension = (typeof THUMBNAIL_EXTENSION)[number]; +export const EXTENSION_TO_MIME: Record< + Uppercase<(typeof THUMBNAIL_EXTENSION)[number]>, + string +> = { + DEFAULT: 'image/jpeg', + JPG: 'image/jpeg', + JPEG: 'image/jpeg', + PNG: 'image/png', + GIF: 'image/gif', + WEBP: 'image/webp', + SVG: 'image/svg+xml', +}; export interface BasicInfo { type: StudyType; diff --git a/src/features/study/group/ui/open-group-modal.tsx b/src/features/study/group/ui/open-group-modal.tsx index ad9fe9db..9d7fa4db 100644 --- a/src/features/study/group/ui/open-group-modal.tsx +++ b/src/features/study/group/ui/open-group-modal.tsx @@ -17,6 +17,7 @@ import { import Step1OpenGroupStudy from './step/step1-group'; import Step2OpenGroupStudy from './step/step2-group'; import Step3OpenGroupStudy from './step/step3-group'; +import { EXTENSION_TO_MIME } from '../api/group-study-types'; import { useCreateGroupStudyMutation } from '../const/use-group-study-mutation'; function Stepper({ step }: { step: 1 | 2 | 3 }) { @@ -80,7 +81,7 @@ export default function OpenGroupStudyModal({ } function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { - const { mutate: createGroupStudy } = useCreateGroupStudyMutation(); + const { mutateAsync: createGroupStudy } = useCreateGroupStudyMutation(); const methods = useForm({ resolver: zodResolver(OpenGroupFormSchema), @@ -123,19 +124,30 @@ function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { if (step > 1) setStep((s) => (s - 1) as 1 | 2 | 3); }; - const onValidSubmit = (values: OpenGroupFormValues) => { - const body = toOpenGroupRequest(values); - - createGroupStudy(body, { - onSuccess: () => { - alert('그룹 스터디 개설이 완료되었습니다!'); - onClose(); - }, - onError: () => { - alert('그룹 스터디 개설 중 오류가 발생했습니다. 다시 시도해 주세요.'); - }, - }); - onClose(); + const onValidSubmit = async (values: OpenGroupFormValues) => { + try { + const body = toOpenGroupRequest(values); + + const created = await createGroupStudy(body); + + const uploadUrl: string | undefined = created?.thumbnailUploadUrl; + const file = values.thumbnailFile; + const ext = + values.thumbnailExtension.toUpperCase() as keyof typeof EXTENSION_TO_MIME; + const contentType = EXTENSION_TO_MIME[ext] || 'application/octet-stream'; + + if (uploadUrl && file) { + await fetch(uploadUrl, { + method: 'PUT', + headers: { 'Content-Type': contentType }, + body: file, + }); + } + alert('그룹 스터디 개설이 완료되었습니다!'); + onClose(); + } catch (err) { + alert('그룹 스터디 개설 중 오류가 발생했습니다. 다시 시도해 주세요.'); + } }; return ( From 59fed8338acf04fbf093acebe6acb074b201c655 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sat, 11 Oct 2025 02:18:30 +0900 Subject: [PATCH 28/31] =?UTF-8?q?fix:=20global.css=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/global.css | 622 ++++++++++++++++++++++++------------------------- 1 file changed, 308 insertions(+), 314 deletions(-) diff --git a/app/global.css b/app/global.css index 1cb1b7fa..4b90e98b 100644 --- a/app/global.css +++ b/app/global.css @@ -341,371 +341,365 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E 0px 4px 32px rgba(0, 0, 0, 0.24), 0px 10px 10px rgba(0, 0, 0, 0.24); } -@layer base { - /* body { - @apply bg-background text-foreground font-sans; - } */ -} - -/* typography */ -@utility font-display-headings1 { - font-size: 72px; - font-weight: var(--font-weight-semibold); - line-height: 108px; - letter-spacing: 0; -} +@layer utilities { + /* typography */ + .font-display-headings1 { + font-size: 72px; + font-weight: var(--font-weight-semibold); + line-height: 108px; + letter-spacing: 0; + } -@utility font-display-headings2 { - font-size: 64px; - font-weight: var(--font-weight-bold); - line-height: 96px; - letter-spacing: 0; -} + .font-display-headings2 { + font-size: 64px; + font-weight: var(--font-weight-bold); + line-height: 96px; + letter-spacing: 0; + } -@utility font-display-headings3 { - font-size: 58px; - font-weight: var(--font-weight-bold); - line-height: 88px; - letter-spacing: 0; -} + .font-display-headings3 { + font-size: 58px; + font-weight: var(--font-weight-bold); + line-height: 88px; + letter-spacing: 0; + } -@utility font-display-headings4 { - font-size: 52px; - font-weight: var(--font-weight-bold); - line-height: 78px; - letter-spacing: 0; -} + .font-display-headings4 { + font-size: 52px; + font-weight: var(--font-weight-bold); + line-height: 78px; + letter-spacing: 0; + } -@utility font-display-headings5 { - font-size: 48px; - font-weight: var(--font-weight-bold); - line-height: 72px; - letter-spacing: 0; -} + .font-display-headings5 { + font-size: 48px; + font-weight: var(--font-weight-bold); + line-height: 72px; + letter-spacing: 0; + } -@utility font-display-headings6 { - font-size: 40px; - font-weight: var(--font-weight-bold); - line-height: 60px; - letter-spacing: 0; -} + .font-display-headings6 { + font-size: 40px; + font-weight: var(--font-weight-bold); + line-height: 60px; + letter-spacing: 0; + } -@utility font-bold-h1 { - font-size: 36px; - font-weight: var(--font-weight-bold); - line-height: 54px; - letter-spacing: 0; -} + .font-bold-h1 { + font-size: 36px; + font-weight: var(--font-weight-bold); + line-height: 54px; + letter-spacing: 0; + } -@utility font-bold-h2 { - font-size: 32px; - font-weight: var(--font-weight-bold); - line-height: 48px; - letter-spacing: 0; -} + .font-bold-h2 { + font-size: 32px; + font-weight: var(--font-weight-bold); + line-height: 48px; + letter-spacing: 0; + } -@utility font-bold-h3 { - font-size: 28px; - font-weight: var(--font-weight-bold); - line-height: 42px; - letter-spacing: 0; -} + .font-bold-h3 { + font-size: 28px; + font-weight: var(--font-weight-bold); + line-height: 42px; + letter-spacing: 0; + } -@utility font-bold-h4 { - font-size: 24px; - font-weight: var(--font-weight-bold); - line-height: 36px; - letter-spacing: 0; -} + .font-bold-h4 { + font-size: 24px; + font-weight: var(--font-weight-bold); + line-height: 36px; + letter-spacing: 0; + } -@utility font-bold-h5 { - font-size: 20px; - font-weight: var(--font-weight-bold); - line-height: 30px; - letter-spacing: 0; -} + .font-bold-h5 { + font-size: 20px; + font-weight: var(--font-weight-bold); + line-height: 30px; + letter-spacing: 0; + } -@utility font-bold-h6 { - font-size: 18px; - font-weight: var(--font-weight-bold); - line-height: 28px; - letter-spacing: 0; -} + .font-bold-h6 { + font-size: 18px; + font-weight: var(--font-weight-bold); + line-height: 28px; + letter-spacing: 0; + } -@utility font-designer-36r { - font-size: 36px; - font-weight: var(--font-weight-regular); - line-height: 54px; - letter-spacing: 0; -} + .font-designer-36r { + font-size: 36px; + font-weight: var(--font-weight-regular); + line-height: 54px; + letter-spacing: 0; + } -@utility font-designer-36m { - font-size: 36px; - font-weight: var(--font-weight-medium); - line-height: 54px; - letter-spacing: 0; -} + .font-designer-36m { + font-size: 36px; + font-weight: var(--font-weight-medium); + line-height: 54px; + letter-spacing: 0; + } -@utility font-designer-36b { - font-size: 36px; - font-weight: var(--font-weight-bold); - line-height: 54px; - letter-spacing: 0; -} + .font-designer-36b { + font-size: 36px; + font-weight: var(--font-weight-bold); + line-height: 54px; + letter-spacing: 0; + } -@utility font-designer-32r { - font-size: 32px; - font-weight: var(--font-weight-regular); - line-height: 48px; - letter-spacing: 0; -} + .font-designer-32r { + font-size: 32px; + font-weight: var(--font-weight-regular); + line-height: 48px; + letter-spacing: 0; + } -@utility font-designer-32m { - font-size: 32px; - font-weight: var(--font-weight-medium); - line-height: 48px; - letter-spacing: 0; -} + .font-designer-32m { + font-size: 32px; + font-weight: var(--font-weight-medium); + line-height: 48px; + letter-spacing: 0; + } -@utility font-designer-32b { - font-size: 32px; - font-weight: var(--font-weight-bold); - line-height: 48px; - letter-spacing: 0; -} + .font-designer-32b { + font-size: 32px; + font-weight: var(--font-weight-bold); + line-height: 48px; + letter-spacing: 0; + } -@utility font-designer-28r { - font-size: 28px; - font-weight: var(--font-weight-regular); - line-height: 42px; - letter-spacing: 0; -} + .font-designer-28r { + font-size: 28px; + font-weight: var(--font-weight-regular); + line-height: 42px; + letter-spacing: 0; + } -@utility font-designer-28m { - font-size: 28px; - font-weight: var(--font-weight-medium); - line-height: 42px; - letter-spacing: 0; -} + .font-designer-28m { + font-size: 28px; + font-weight: var(--font-weight-medium); + line-height: 42px; + letter-spacing: 0; + } -@utility font-designer-28b { - font-size: 28px; - font-weight: var(--font-weight-bold); - line-height: 42px; - letter-spacing: 0; -} + .font-designer-28b { + font-size: 28px; + font-weight: var(--font-weight-bold); + line-height: 42px; + letter-spacing: 0; + } -@utility font-designer-24r { - font-size: 24px; - font-weight: var(--font-weight-regular); - line-height: 37px; - letter-spacing: 0; -} + .font-designer-24r { + font-size: 24px; + font-weight: var(--font-weight-regular); + line-height: 37px; + letter-spacing: 0; + } -@utility font-designer-24m { - font-size: 24px; - font-weight: var(--font-weight-medium); - line-height: 37px; - letter-spacing: 0; -} + .font-designer-24m { + font-size: 24px; + font-weight: var(--font-weight-medium); + line-height: 37px; + letter-spacing: 0; + } -@utility font-designer-24b { - font-size: 24px; - font-weight: var(--font-weight-bold); - line-height: 37px; - letter-spacing: 0; -} + .font-designer-24b { + font-size: 24px; + font-weight: var(--font-weight-bold); + line-height: 37px; + letter-spacing: 0; + } -@utility font-designer-20r { - font-size: 20px; - font-weight: var(--font-weight-regular); - line-height: 30px; - letter-spacing: 0; -} + .font-designer-20r { + font-size: 20px; + font-weight: var(--font-weight-regular); + line-height: 30px; + letter-spacing: 0; + } -@utility font-designer-20m { - font-size: 20px; - font-weight: var(--font-weight-medium); - line-height: 30px; - letter-spacing: 0; -} + .font-designer-20m { + font-size: 20px; + font-weight: var(--font-weight-medium); + line-height: 30px; + letter-spacing: 0; + } -@utility font-designer-20b { - font-size: 20px; - font-weight: var(--font-weight-bold); - line-height: 30px; - letter-spacing: 0; -} + .font-designer-20b { + font-size: 20px; + font-weight: var(--font-weight-bold); + line-height: 30px; + letter-spacing: 0; + } -@utility font-designer-18r { - font-size: 18px; - font-weight: var(--font-weight-regular); - line-height: 29px; - letter-spacing: 0; -} + .font-designer-18r { + font-size: 18px; + font-weight: var(--font-weight-regular); + line-height: 29px; + letter-spacing: 0; + } -@utility font-designer-18m { - font-size: 18px; - font-weight: var(--font-weight-medium); - line-height: 29px; - letter-spacing: 0; -} + .font-designer-18m { + font-size: 18px; + font-weight: var(--font-weight-medium); + line-height: 29px; + letter-spacing: 0; + } -@utility font-designer-18b { - font-size: 18px; - font-weight: var(--font-weight-bold); - line-height: 29px; - letter-spacing: 0; -} + .font-designer-18b { + font-size: 18px; + font-weight: var(--font-weight-bold); + line-height: 29px; + letter-spacing: 0; + } -@utility font-designer-16r { - font-size: 16px; - font-weight: var(--font-weight-regular); - line-height: 25px; - letter-spacing: 0; -} + .font-designer-16r { + font-size: 16px; + font-weight: var(--font-weight-regular); + line-height: 25px; + letter-spacing: 0; + } -@utility font-designer-16m { - font-size: 16px; - font-weight: var(--font-weight-medium); - line-height: 25px; - letter-spacing: 0; -} + .font-designer-16m { + font-size: 16px; + font-weight: var(--font-weight-medium); + line-height: 25px; + letter-spacing: 0; + } -@utility font-designer-16b { - font-size: 16px; - font-weight: var(--font-weight-bold); - line-height: 25px; - letter-spacing: 0; -} + .font-designer-16b { + font-size: 16px; + font-weight: var(--font-weight-bold); + line-height: 25px; + letter-spacing: 0; + } -@utility font-designer-15r { - font-size: 15px; - font-weight: var(--font-weight-regular); - line-height: 23px; - letter-spacing: 0; -} + .font-designer-15r { + font-size: 15px; + font-weight: var(--font-weight-regular); + line-height: 23px; + letter-spacing: 0; + } -@utility font-designer-15m { - font-size: 15px; - font-weight: var(--font-weight-medium); - line-height: 23px; - letter-spacing: 0; -} + .font-designer-15m { + font-size: 15px; + font-weight: var(--font-weight-medium); + line-height: 23px; + letter-spacing: 0; + } -@utility font-designer-15b { - font-size: 15px; - font-weight: var(--font-weight-bold); - line-height: 23px; - letter-spacing: 0; -} + .font-designer-15b { + font-size: 15px; + font-weight: var(--font-weight-bold); + line-height: 23px; + letter-spacing: 0; + } -@utility font-designer-14r { - font-size: 14px; - font-weight: var(--font-weight-regular); - line-height: 22px; - letter-spacing: 0; -} + .font-designer-14r { + font-size: 14px; + font-weight: var(--font-weight-regular); + line-height: 22px; + letter-spacing: 0; + } -@utility font-designer-14m { - font-size: 14px; - font-weight: var(--font-weight-medium); - line-height: 22px; - letter-spacing: 0; -} + .font-designer-14m { + font-size: 14px; + font-weight: var(--font-weight-medium); + line-height: 22px; + letter-spacing: 0; + } -@utility font-designer-14b { - font-size: 14px; - font-weight: var(--font-weight-bold); - line-height: 22px; - letter-spacing: 0; -} + .font-designer-14b { + font-size: 14px; + font-weight: var(--font-weight-bold); + line-height: 22px; + letter-spacing: 0; + } -@utility font-designer-13r { - font-size: 13px; - font-weight: var(--font-weight-regular); - line-height: 20px; - letter-spacing: 0; -} + .font-designer-13r { + font-size: 13px; + font-weight: var(--font-weight-regular); + line-height: 20px; + letter-spacing: 0; + } -@utility font-designer-13m { - font-size: 13px; - font-weight: var(--font-weight-medium); - line-height: 20px; - letter-spacing: 0; -} + .font-designer-13m { + font-size: 13px; + font-weight: var(--font-weight-medium); + line-height: 20px; + letter-spacing: 0; + } -@utility font-designer-13b { - font-size: 13px; - font-weight: var(--font-weight-bold); - line-height: 20px; - letter-spacing: 0; -} + .font-designer-13b { + font-size: 13px; + font-weight: var(--font-weight-bold); + line-height: 20px; + letter-spacing: 0; + } -@utility font-designer-12r { - font-size: 12px; - font-weight: var(--font-weight-regular); - line-height: 19px; - letter-spacing: 0; -} + .font-designer-12r { + font-size: 12px; + font-weight: var(--font-weight-regular); + line-height: 19px; + letter-spacing: 0; + } -@utility font-designer-12m { - font-size: 12px; - font-weight: var(--font-weight-medium); - line-height: 19px; - letter-spacing: 0; -} + .font-designer-12m { + font-size: 12px; + font-weight: var(--font-weight-medium); + line-height: 19px; + letter-spacing: 0; + } -@utility font-designer-12b { - font-size: 12px; - font-weight: var(--font-weight-bold); - line-height: 19px; - letter-spacing: 0; -} + .font-designer-12b { + font-size: 12px; + font-weight: var(--font-weight-bold); + line-height: 19px; + letter-spacing: 0; + } -@utility font-designer-11r { - font-size: 11px; - font-weight: var(--font-weight-regular); - line-height: 18px; - letter-spacing: 0; -} + .font-designer-11r { + font-size: 11px; + font-weight: var(--font-weight-regular); + line-height: 18px; + letter-spacing: 0; + } -@utility font-designer-11m { - font-size: 11px; - font-weight: var(--font-weight-medium); - line-height: 18px; - letter-spacing: 0; -} + .font-designer-11m { + font-size: 11px; + font-weight: var(--font-weight-medium); + line-height: 18px; + letter-spacing: 0; + } -@utility font-designer-11b { - font-size: 11px; - font-weight: var(--font-weight-bold); - line-height: 18px; - letter-spacing: 0; -} + .font-designer-11b { + font-size: 11px; + font-weight: var(--font-weight-bold); + line-height: 18px; + letter-spacing: 0; + } -@utility font-designer-10r { - font-size: 10px; - font-weight: var(--font-weight-regular); - line-height: 17px; - letter-spacing: 0; -} + .font-designer-10r { + font-size: 10px; + font-weight: var(--font-weight-regular); + line-height: 17px; + letter-spacing: 0; + } -@utility font-designer-10m { - font-size: 10px; - font-weight: var(--font-weight-medium); - line-height: 17px; - letter-spacing: 0; -} + .font-designer-10m { + font-size: 10px; + font-weight: var(--font-weight-medium); + line-height: 17px; + letter-spacing: 0; + } -@utility font-designer-10b { - font-size: 10px; - font-weight: var(--font-weight-bold); - line-height: 17px; - letter-spacing: 0; -} + .font-designer-10b { + font-size: 10px; + font-weight: var(--font-weight-bold); + line-height: 17px; + letter-spacing: 0; + } -@layer utilities { .scrollbar-hide { &::-webkit-scrollbar { display: none; From 442e922ea23132a45e2d3ebd0ec92f86740ad3f2 Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sat, 11 Oct 2025 03:18:36 +0900 Subject: [PATCH 29/31] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/ui/open-group-modal.tsx | 21 ++++++++++++------- .../study/group/ui/step/step2-group.tsx | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/features/study/group/ui/open-group-modal.tsx b/src/features/study/group/ui/open-group-modal.tsx index 9d7fa4db..814b341d 100644 --- a/src/features/study/group/ui/open-group-modal.tsx +++ b/src/features/study/group/ui/open-group-modal.tsx @@ -130,18 +130,23 @@ function OpenGroupStudyForm({ onClose }: { onClose: () => void }) { const created = await createGroupStudy(body); - const uploadUrl: string | undefined = created?.thumbnailUploadUrl; - const file = values.thumbnailFile; - const ext = - values.thumbnailExtension.toUpperCase() as keyof typeof EXTENSION_TO_MIME; - const contentType = EXTENSION_TO_MIME[ext] || 'application/octet-stream'; + const uploadUrl: string = created.content.thumbnailUploadUrl; + const file = methods.getValues('thumbnailFile'); if (uploadUrl && file) { - await fetch(uploadUrl, { + const formData = new FormData(); + formData.append('file', file); + + const res = await fetch(uploadUrl, { method: 'PUT', - headers: { 'Content-Type': contentType }, - body: file, + body: formData, }); + + if (!res.ok) { + console.error('파일 업로드 실패:', res.status, res.statusText); + } else { + console.log('파일 업로드 성공!'); + } } alert('그룹 스터디 개설이 완료되었습니다!'); onClose(); diff --git a/src/features/study/group/ui/step/step2-group.tsx b/src/features/study/group/ui/step/step2-group.tsx index 74548e05..572f823c 100644 --- a/src/features/study/group/ui/step/step2-group.tsx +++ b/src/features/study/group/ui/step/step2-group.tsx @@ -44,7 +44,7 @@ export default function Step2OpenGroupStudy() { : 'DEFAULT'; setValue('thumbnailExtension', validExt, { shouldValidate: true }); - setValue('thumbnailFile', file); + setValue('thumbnailFile', file, { shouldValidate: true }); setImage(URL.createObjectURL(file)); }; From 650193ca1adcd40ec293fdb3e4c1efb39315130c Mon Sep 17 00:00:00 2001 From: Mimiminz Date: Sat, 11 Oct 2025 03:35:37 +0900 Subject: [PATCH 30/31] =?UTF-8?q?fix:=20=EC=BB=A8=ED=94=8C=EB=A6=AD?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/group/ui/group-study-list.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/study/group/ui/group-study-list.tsx b/src/features/study/group/ui/group-study-list.tsx index 28aa91f2..ee9e4df7 100644 --- a/src/features/study/group/ui/group-study-list.tsx +++ b/src/features/study/group/ui/group-study-list.tsx @@ -1,10 +1,10 @@ 'use client'; import { useInfiniteQuery } from '@tanstack/react-query'; +import Image from 'next/image'; import React, { Fragment } from 'react'; import Badge from '@/shared/ui/badge'; import { getGroupStudyList } from '../api/get-group-study-list'; -import { BasicInfo } from '../api/group-study-types'; -import Image from 'next/image'; +import { DetailBasicInfo } from '../api/group-study-types'; enum Method { ONLINE = '온라인', @@ -52,7 +52,7 @@ export default function GroupStudyList() { }); const basicInfoItems = ( - basicInfo: BasicInfo, + basicInfo: DetailBasicInfo, currentParticipantCount: number, ) => [ { From d0da49bd99c4a5578aa469f02d52d18d540c5a51 Mon Sep 17 00:00:00 2001 From: MinJu Jo Date: Sat, 11 Oct 2025 11:15:17 +0900 Subject: [PATCH 31/31] =?UTF-8?q?fix:=20global=20css=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/global.css | 377 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 376 insertions(+), 1 deletion(-) diff --git a/app/global.css b/app/global.css index 4b90e98b..6a5591ba 100644 --- a/app/global.css +++ b/app/global.css @@ -341,7 +341,382 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E 0px 4px 32px rgba(0, 0, 0, 0.24), 0px 10px 10px rgba(0, 0, 0, 0.24); } +@layer base { + /* body { + @apply bg-background text-foreground font-sans; + } */ +} + +/* typography */ +@utility font-display-headings1 { + font-size: 72px; + font-weight: var(--font-weight-semibold); + line-height: 108px; + letter-spacing: 0; +} + +@utility font-display-headings2 { + font-size: 64px; + font-weight: var(--font-weight-bold); + line-height: 96px; + letter-spacing: 0; +} + +@utility font-display-headings3 { + font-size: 58px; + font-weight: var(--font-weight-bold); + line-height: 88px; + letter-spacing: 0; +} + +@utility font-display-headings4 { + font-size: 52px; + font-weight: var(--font-weight-bold); + line-height: 78px; + letter-spacing: 0; +} + +@utility font-display-headings5 { + font-size: 48px; + font-weight: var(--font-weight-bold); + line-height: 72px; + letter-spacing: 0; +} + +@utility font-display-headings6 { + font-size: 40px; + font-weight: var(--font-weight-bold); + line-height: 60px; + letter-spacing: 0; +} + +@utility font-bold-h1 { + font-size: 36px; + font-weight: var(--font-weight-bold); + line-height: 54px; + letter-spacing: 0; +} + +@utility font-bold-h2 { + font-size: 32px; + font-weight: var(--font-weight-bold); + line-height: 48px; + letter-spacing: 0; +} + +@utility font-bold-h3 { + font-size: 28px; + font-weight: var(--font-weight-bold); + line-height: 42px; + letter-spacing: 0; +} + +@utility font-bold-h4 { + font-size: 24px; + font-weight: var(--font-weight-bold); + line-height: 36px; + letter-spacing: 0; +} + +@utility font-bold-h5 { + font-size: 20px; + font-weight: var(--font-weight-bold); + line-height: 30px; + letter-spacing: 0; +} + +@utility font-bold-h6 { + font-size: 18px; + font-weight: var(--font-weight-bold); + line-height: 28px; + letter-spacing: 0; +} + +@utility font-designer-36r { + font-size: 36px; + font-weight: var(--font-weight-regular); + line-height: 54px; + letter-spacing: 0; +} + +@utility font-designer-36m { + font-size: 36px; + font-weight: var(--font-weight-medium); + line-height: 54px; + letter-spacing: 0; +} + +@utility font-designer-36b { + font-size: 36px; + font-weight: var(--font-weight-bold); + line-height: 54px; + letter-spacing: 0; +} + +@utility font-designer-32r { + font-size: 32px; + font-weight: var(--font-weight-regular); + line-height: 48px; + letter-spacing: 0; +} + +@utility font-designer-32m { + font-size: 32px; + font-weight: var(--font-weight-medium); + line-height: 48px; + letter-spacing: 0; +} + +@utility font-designer-32b { + font-size: 32px; + font-weight: var(--font-weight-bold); + line-height: 48px; + letter-spacing: 0; +} + +@utility font-designer-28r { + font-size: 28px; + font-weight: var(--font-weight-regular); + line-height: 42px; + letter-spacing: 0; +} + +@utility font-designer-28m { + font-size: 28px; + font-weight: var(--font-weight-medium); + line-height: 42px; + letter-spacing: 0; +} + +@utility font-designer-28b { + font-size: 28px; + font-weight: var(--font-weight-bold); + line-height: 42px; + letter-spacing: 0; +} + +@utility font-designer-24r { + font-size: 24px; + font-weight: var(--font-weight-regular); + line-height: 37px; + letter-spacing: 0; +} + +@utility font-designer-24m { + font-size: 24px; + font-weight: var(--font-weight-medium); + line-height: 37px; + letter-spacing: 0; +} + +@utility font-designer-24b { + font-size: 24px; + font-weight: var(--font-weight-bold); + line-height: 37px; + letter-spacing: 0; +} + +@utility font-designer-20r { + font-size: 20px; + font-weight: var(--font-weight-regular); + line-height: 30px; + letter-spacing: 0; +} + +@utility font-designer-20m { + font-size: 20px; + font-weight: var(--font-weight-medium); + line-height: 30px; + letter-spacing: 0; +} + +@utility font-designer-20b { + font-size: 20px; + font-weight: var(--font-weight-bold); + line-height: 30px; + letter-spacing: 0; +} + +@utility font-designer-18r { + font-size: 18px; + font-weight: var(--font-weight-regular); + line-height: 29px; + letter-spacing: 0; +} + +@utility font-designer-18m { + font-size: 18px; + font-weight: var(--font-weight-medium); + line-height: 29px; + letter-spacing: 0; +} + +@utility font-designer-18b { + font-size: 18px; + font-weight: var(--font-weight-bold); + line-height: 29px; + letter-spacing: 0; +} + +@utility font-designer-16r { + font-size: 16px; + font-weight: var(--font-weight-regular); + line-height: 25px; + letter-spacing: 0; +} + +@utility font-designer-16m { + font-size: 16px; + font-weight: var(--font-weight-medium); + line-height: 25px; + letter-spacing: 0; +} + +@utility font-designer-16b { + font-size: 16px; + font-weight: var(--font-weight-bold); + line-height: 25px; + letter-spacing: 0; +} + +@utility font-designer-15r { + font-size: 15px; + font-weight: var(--font-weight-regular); + line-height: 23px; + letter-spacing: 0; +} + +@utility font-designer-15m { + font-size: 15px; + font-weight: var(--font-weight-medium); + line-height: 23px; + letter-spacing: 0; +} + +@utility font-designer-15b { + font-size: 15px; + font-weight: var(--font-weight-bold); + line-height: 23px; + letter-spacing: 0; +} + +@utility font-designer-14r { + font-size: 14px; + font-weight: var(--font-weight-regular); + line-height: 22px; + letter-spacing: 0; +} + +@utility font-designer-14m { + font-size: 14px; + font-weight: var(--font-weight-medium); + line-height: 22px; + letter-spacing: 0; +} + +@utility font-designer-14b { + font-size: 14px; + font-weight: var(--font-weight-bold); + line-height: 22px; + letter-spacing: 0; +} + +@utility font-designer-13r { + font-size: 13px; + font-weight: var(--font-weight-regular); + line-height: 20px; + letter-spacing: 0; +} + +@utility font-designer-13m { + font-size: 13px; + font-weight: var(--font-weight-medium); + line-height: 20px; + letter-spacing: 0; +} + +@utility font-designer-13b { + font-size: 13px; + font-weight: var(--font-weight-bold); + line-height: 20px; + letter-spacing: 0; +} + +@utility font-designer-12r { + font-size: 12px; + font-weight: var(--font-weight-regular); + line-height: 19px; + letter-spacing: 0; +} + +@utility font-designer-12m { + font-size: 12px; + font-weight: var(--font-weight-medium); + line-height: 19px; + letter-spacing: 0; +} + +@utility font-designer-12b { + font-size: 12px; + font-weight: var(--font-weight-bold); + line-height: 19px; + letter-spacing: 0; +} + +@utility font-designer-11r { + font-size: 11px; + font-weight: var(--font-weight-regular); + line-height: 18px; + letter-spacing: 0; +} + +@utility font-designer-11m { + font-size: 11px; + font-weight: var(--font-weight-medium); + line-height: 18px; + letter-spacing: 0; +} + +@utility font-designer-11b { + font-size: 11px; + font-weight: var(--font-weight-bold); + line-height: 18px; + letter-spacing: 0; +} + +@utility font-designer-10r { + font-size: 10px; + font-weight: var(--font-weight-regular); + line-height: 17px; + letter-spacing: 0; +} + +@utility font-designer-10m { + font-size: 10px; + font-weight: var(--font-weight-medium); + line-height: 17px; + letter-spacing: 0; +} + +@utility font-designer-10b { + font-size: 10px; + font-weight: var(--font-weight-bold); + line-height: 17px; + letter-spacing: 0; +} + @layer utilities { + .scrollbar-hide { + &::-webkit-scrollbar { + display: none; + } + scrollbar-width: none; + } +} + + +/* 자동완성이 안되는 문제가 있어 잠시 주석처리 합니다 */ +/* @layer utilities { /* typography */ .font-display-headings1 { font-size: 72px; @@ -706,7 +1081,7 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E } scrollbar-width: none; } -} +} */ /* shadcn 색상 테마 */