From ead1ed6f93db0a67128833a772e5a05982c770a1 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Tue, 21 Jan 2025 01:16:07 +0900
Subject: [PATCH 01/32] =?UTF-8?q?[YS-172]=20chore:=20zod=20@hookform/resol?=
=?UTF-8?q?vers=20=EC=84=A4=EC=B9=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package.json | 4 +++-
pnpm-lock.yaml | 20 ++++++++++++++++++++
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 8f5ce81..8e07b87 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
},
"dependencies": {
"@emotion/react": "^11.14.0",
+ "@hookform/resolvers": "^3.10.0",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-select": "^2.1.4",
@@ -24,9 +25,10 @@
"jira-prepare-commit-msg": "^1.7.2",
"next": "14.2.22",
"react": "^18",
+ "react-day-picker": "^9.5.0",
"react-dom": "^18",
"react-hook-form": "^7.54.2",
- "react-day-picker": "^9.5.0"
+ "zod": "^3.24.1"
},
"devDependencies": {
"@mswjs/http-middleware": "^0.10.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d94c520..f23459a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
'@emotion/react':
specifier: ^11.14.0
version: 11.14.0(@types/react@18.3.18)(react@18.3.1)
+ '@hookform/resolvers':
+ specifier: ^3.10.0
+ version: 3.10.0(react-hook-form@7.54.2(react@18.3.1))
'@radix-ui/react-dialog':
specifier: ^1.1.4
version: 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -53,6 +56,9 @@ importers:
react-hook-form:
specifier: ^7.54.2
version: 7.54.2(react@18.3.1)
+ zod:
+ specifier: ^3.24.1
+ version: 3.24.1
devDependencies:
'@mswjs/http-middleware':
specifier: ^0.10.2
@@ -220,6 +226,11 @@ packages:
'@floating-ui/utils@0.2.8':
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
+ '@hookform/resolvers@3.10.0':
+ resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==}
+ peerDependencies:
+ react-hook-form: ^7.0.0
+
'@humanwhocodes/config-array@0.13.0':
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
engines: {node: '>=10.10.0'}
@@ -2439,6 +2450,9 @@ packages:
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
engines: {node: '>=18'}
+ zod@3.24.1:
+ resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
+
snapshots:
'@babel/code-frame@7.26.2':
@@ -2616,6 +2630,10 @@ snapshots:
'@floating-ui/utils@0.2.8': {}
+ '@hookform/resolvers@3.10.0(react-hook-form@7.54.2(react@18.3.1))':
+ dependencies:
+ react-hook-form: 7.54.2(react@18.3.1)
+
'@humanwhocodes/config-array@0.13.0':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
@@ -5099,3 +5117,5 @@ snapshots:
yocto-queue@0.1.0: {}
yoctocolors-cjs@2.1.2: {}
+
+ zod@3.24.1: {}
From dcfee313d7892662685865ba4976981d1e70943b Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Wed, 22 Jan 2025 22:23:32 +0900
Subject: [PATCH 02/32] =?UTF-8?q?[YS-172]=20feat:=20=EA=B3=B5=EA=B3=A0=20?=
=?UTF-8?q?=EB=93=B1=EB=A1=9D=20zod=20schema=20=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../OutlineSection/OutlineSection.tsx | 2 +-
.../UploadContainer/UploadContainer.tsx | 58 ++++++------
.../upload/hooks/useUploadExperiemntPost.tsx | 36 ++++++++
.../upload/uploadExperimentPostSchema.ts | 89 +++++++++++++++++++
4 files changed, 158 insertions(+), 27 deletions(-)
create mode 100644 src/app/upload/hooks/useUploadExperiemntPost.tsx
create mode 100644 src/schema/upload/uploadExperimentPostSchema.ts
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index 048ae5f..b98c516 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -13,7 +13,7 @@ import { headingIcon, input, label } from '../UploadContainer/UploadContainer';
import DatePickerField from '@/app/upload/components/DatePickerField/DatePickerField';
import { colors } from '@/styles/colors';
-enum MatchType {
+export enum MatchType {
OFFLINE = 'OFFLINE',
ONLINE = 'ONLINE',
HYBRID = 'HYBRID',
diff --git a/src/app/upload/components/UploadContainer/UploadContainer.tsx b/src/app/upload/components/UploadContainer/UploadContainer.tsx
index 671b775..035abec 100644
--- a/src/app/upload/components/UploadContainer/UploadContainer.tsx
+++ b/src/app/upload/components/UploadContainer/UploadContainer.tsx
@@ -4,40 +4,46 @@ import { Theme } from '@emotion/react';
import { css } from '@emotion/react';
import Link from 'next/link';
import React from 'react';
+import { FormProvider } from 'react-hook-form';
+import useUploadExperimentPost from '../../hooks/useUploadExperiemntPost';
import ApplyMethodSection from '../ApplyMethodSection/ApplyMethodSection';
import DescriptionSection from '../DescriptionSection/DescriptionSection';
import OutlineSection from '../OutlineSection/OutlineSection';
const UploadContainer = () => {
- return (
-
-
-
실험에 대한 정보를 입력해 주세요
-
구체적일수록 참여자 매칭 확률이 높아져요
-
-
-
- {/* 실험 개요 */}
-
-
- {/* 실험 설명 */}
-
+ const { form, handleSubmit } = useUploadExperimentPost();
- {/* 실험 참여 방법 */}
-
-
-
- {/* 버튼 */}
-
-
-
-
-
-
-
+ return (
+
+
+
+
실험에 대한 정보를 입력해 주세요
+
구체적일수록 참여자 매칭 확률이 높아져요
+
+
+
+ {/* 실험 개요 */}
+
+
+ {/* 실험 설명 */}
+
+
+ {/* 실험 참여 방법 */}
+
+
+
+ {/* 버튼 */}
+
+
+
+
+
+
-
+
);
};
diff --git a/src/app/upload/hooks/useUploadExperiemntPost.tsx b/src/app/upload/hooks/useUploadExperiemntPost.tsx
new file mode 100644
index 0000000..0a46b0e
--- /dev/null
+++ b/src/app/upload/hooks/useUploadExperiemntPost.tsx
@@ -0,0 +1,36 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+
+import UploadExperimentPostSchema, {
+ UploadExperimentPostSchemaType,
+} from '@/schema/upload/uploadExperimentPostSchema';
+
+const useUploadExperimentPost = () => {
+ const form = useForm
({
+ mode: 'onBlur',
+ reValidateMode: 'onBlur',
+ resolver: zodResolver(UploadExperimentPostSchema()),
+ defaultValues: {
+ leadResearcher: '',
+ startDate: undefined,
+ endDate: undefined,
+ },
+ });
+
+ const handleSubmit = async (data: UploadExperimentPostSchemaType) => {
+ try {
+ // console.log('공고 등록 form >> ', data);
+
+ await form.reset();
+ } catch (error) {
+ // console.error('공고 등록 form 저장 중 오류 발생', error);
+ }
+ };
+
+ return {
+ form,
+ handleSubmit: form.handleSubmit(handleSubmit),
+ };
+};
+
+export default useUploadExperimentPost;
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
new file mode 100644
index 0000000..483735d
--- /dev/null
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -0,0 +1,89 @@
+import { z } from 'zod';
+
+import { MatchType } from '@/app/upload/components/OutlineSection/OutlineSection';
+
+export type UploadExperimentPostSchemaType = z.infer>;
+
+const UploadExperimentPostSchema = () => {
+ return z.object({
+ // targetGroupInfo: z.object({
+ // startAge: z.number().min(0, '0세 이상'), // 참여 가능 나이 (이상)
+ // endAge: z.number().min(0, '0세 이상'), // 참여 가능 나이 (이하)
+ // genderType: z.nativeEnum(GenderType), // 성별
+ // otherCondition: z.string().optional(), // 기타조건
+ // }),
+ // applyMethodInfo: z.object({
+ // content: z.string().nonempty('필수 값'), // 참여 방법
+ // formUrl: z.string().url().optional(), // 링크
+ // phoneNum: z.string().optional(), // 연락처
+ // }),
+ // imageListInfo: z.object({
+ // images: z.array(z.string()).optional(), // 이미지 목록 (최대 3장)
+ // }),
+
+ // 실험 시작 날짜
+ startDate: z.union(
+ [
+ z.date({ errorMap: () => ({ message: '' }) }),
+ z.null({ errorMap: () => ({ message: '' }) }),
+ ],
+ {
+ required_error: '',
+ invalid_type_error: '',
+ },
+ ),
+
+ // 실험 종료 날짜
+ endDate: z.union(
+ [
+ z.date({ errorMap: () => ({ message: '' }) }),
+ z.null({ errorMap: () => ({ message: '' }) }),
+ ],
+ {
+ required_error: '',
+ invalid_type_error: '',
+ },
+ ),
+
+ // 진행 방식
+ matchType: z.nativeEnum(MatchType),
+ // 실험 횟수
+ count: z.enum(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']).transform(Number), // 참여 횟수
+ // 소요 시간
+ timeRequired: z.union([
+ z.enum([
+ 'LESS_30M',
+ 'ABOUT_30M',
+ 'ABOUT_1H',
+ 'ABOUT_1H30M',
+ 'ABOUT_2H',
+ 'ABOUT_2H30M',
+ 'ABOUT_3H',
+ 'ABOUT_3H30M',
+ 'ABOUT_4H',
+ ]),
+ z.null(),
+ ]),
+ // 연구 책임자
+ leadResearcher: z
+ .string()
+ .min(10, { message: '최소 10자 이상으로 입력해 주세요' })
+ .max(150, { message: '최대 150자 이하로 입력해 주세요' }),
+ // 대학교
+ univName: z.string().nonempty('대학교 이름 필수'),
+ // 지역
+ region: z.string().nonempty('지역 필수'),
+ // 지역구
+ area: z.string().nonempty('지역구 필수'),
+ // 상세 주소
+ detailedAddress: z.string().optional(),
+ // 보상
+ reward: z.string().nonempty('보상 필수'),
+
+ // title: z.string().nonempty('실험 제목 필수'), // 실험 제목
+ // content: z.string().nonempty('실험 본문 필수'), // 실험 본문
+ // alarmAgree: z.boolean().default(false), // 알람 동의
+ });
+};
+
+export default UploadExperimentPostSchema;
From 942d466825b04a82480d38765ba540ca592b7da7 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Wed, 22 Jan 2025 22:27:00 +0900
Subject: [PATCH 03/32] =?UTF-8?q?[YS-172]=20refactor:=20useUploadExperimen?=
=?UTF-8?q?tPost=20=EC=98=A4=ED=83=88=EC=9E=90=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/upload/components/UploadContainer/UploadContainer.tsx | 2 +-
...{useUploadExperiemntPost.tsx => useUploadExperimentPost.tsx} | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename src/app/upload/hooks/{useUploadExperiemntPost.tsx => useUploadExperimentPost.tsx} (100%)
diff --git a/src/app/upload/components/UploadContainer/UploadContainer.tsx b/src/app/upload/components/UploadContainer/UploadContainer.tsx
index 035abec..2d24dfa 100644
--- a/src/app/upload/components/UploadContainer/UploadContainer.tsx
+++ b/src/app/upload/components/UploadContainer/UploadContainer.tsx
@@ -6,7 +6,7 @@ import Link from 'next/link';
import React from 'react';
import { FormProvider } from 'react-hook-form';
-import useUploadExperimentPost from '../../hooks/useUploadExperiemntPost';
+import useUploadExperimentPost from '../../hooks/useUploadExperimentPost';
import ApplyMethodSection from '../ApplyMethodSection/ApplyMethodSection';
import DescriptionSection from '../DescriptionSection/DescriptionSection';
import OutlineSection from '../OutlineSection/OutlineSection';
diff --git a/src/app/upload/hooks/useUploadExperiemntPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
similarity index 100%
rename from src/app/upload/hooks/useUploadExperiemntPost.tsx
rename to src/app/upload/hooks/useUploadExperimentPost.tsx
From 7b2b3b02b73f5ccdf5f6b924c5347d1ee83f9967 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Wed, 22 Jan 2025 23:16:19 +0900
Subject: [PATCH 04/32] =?UTF-8?q?[YS-172]=20chore:=20DatePickerForm=20?=
=?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20react-hook-f?=
=?UTF-8?q?orm=20=EC=97=B0=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DatePickerField/DatePickerField.tsx | 302 ------------------
.../DatePickerForm/DatePickerForm.styles.ts | 222 +++++++++++++
.../DatePickerForm/DatePickerForm.tsx | 120 +++++++
src/app/upload/upload.utils.ts | 10 +
4 files changed, 352 insertions(+), 302 deletions(-)
delete mode 100644 src/app/upload/components/DatePickerField/DatePickerField.tsx
create mode 100644 src/app/upload/components/DatePickerForm/DatePickerForm.styles.ts
create mode 100644 src/app/upload/components/DatePickerForm/DatePickerForm.tsx
create mode 100644 src/app/upload/upload.utils.ts
diff --git a/src/app/upload/components/DatePickerField/DatePickerField.tsx b/src/app/upload/components/DatePickerField/DatePickerField.tsx
deleted file mode 100644
index acc1a7a..0000000
--- a/src/app/upload/components/DatePickerField/DatePickerField.tsx
+++ /dev/null
@@ -1,302 +0,0 @@
-import { css, Theme } from '@emotion/react';
-import * as Popover from '@radix-ui/react-popover';
-import { ko } from 'date-fns/locale';
-import React, { useState } from 'react';
-import { DayPicker, DateRange } from 'react-day-picker';
-
-import 'react-day-picker/dist/style.css';
-
-import Icon from '@/components/Icon';
-import { colors } from '@/styles/colors';
-
-interface DatePickerFieldProps {
- placeholder: string;
- onDateChange: (dates: DateRange) => void;
- experimentDateChecked?: boolean;
-}
-
-export type NullableDate = Date | null;
-
-const DatePickerField = ({
- placeholder,
- onDateChange,
- experimentDateChecked = false,
-}: DatePickerFieldProps) => {
- const [isOpen, setIsOpen] = useState(false);
-
- const [selectedDates, setSelectedDates] = useState({
- from: undefined,
- to: undefined,
- });
-
- const handleDateChange = (range: DateRange) => {
- setSelectedDates(range);
- onDateChange(range);
- };
-
- return (
-
-
-
- datePickerField(theme, experimentDateChecked, isOpen)}
- role="button"
- tabIndex={0}
- >
- placeholderText(theme, !!selectedDates.from, experimentDateChecked)}
- >
- {!experimentDateChecked
- ? selectedDates.from
- ? selectedDates.from?.toLocaleDateString() ===
- selectedDates.to?.toLocaleDateString()
- ? `${selectedDates.from?.toLocaleDateString()}`
- : `${selectedDates.from?.toLocaleDateString()} ~ ${selectedDates.to?.toLocaleDateString()}`
- : placeholder
- : '본문 참고'}
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default DatePickerField;
-
-const datePickerFieldContainer = css`
- position: relative;
-`;
-
-const datePickerField = (theme: Theme, experimentDateChecked: boolean, isOpen: boolean) => css`
- width: 100%;
- height: 4.8rem;
-
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- padding: 1.3rem 1.6rem;
-
- border: 0.1rem solid
- ${experimentDateChecked
- ? theme.colors.line01
- : isOpen
- ? theme.colors.lineTinted
- : theme.colors.line01};
- border-radius: 1.2rem;
-
- background-color: ${experimentDateChecked ? theme.colors.field02 : colors.field01};
-
- cursor: ${experimentDateChecked ? 'not-allowed' : 'pointer'};
-`;
-
-const placeholderText = (
- theme: Theme,
- bothDatesSelected: boolean,
- experimentDateChecked: boolean,
-) => css`
- ${theme.fonts.label.large.R14};
- color: ${experimentDateChecked
- ? theme.colors.text02
- : bothDatesSelected
- ? theme.colors.text06
- : theme.colors.text02};
-
- flex: 1;
-
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-`;
-
-const iconStyle = css`
- margin-left: 1rem;
-`;
-
-const popoverLayout = (theme: Theme) => css`
- background-color: ${colors.field01};
- border: 0.1rem solid ${colors.line01};
- border-radius: 8px;
- padding: 1rem;
- z-index: ${theme.zIndex.datePickerPopup};
-
- width: 45.2rem;
-
- box-shadow: 0rem 0.4rem 1rem rgba(0, 0, 0, 0.1);
-`;
-
-const datepickerCustom = (theme: Theme) => css`
- .rdp-months {
- width: 40rem;
- position: relative;
- padding-top: 1.2rem;
- }
-
- .rdp-month {
- display: flex;
- flex-flow: column nowrap;
- }
-
- .rdp-nav {
- position: absolute;
- top: 1.2rem;
- right: 50%;
- transform: translate(50%);
-
- display: flex;
- flex-flow: row nowrap;
- justify-content: center;
- gap: 24.8rem;
- }
-
- .rdp-chevron {
- fill: ${theme.colors.icon03};
- }
-
- .rdp-month_caption {
- display: flex;
- flex-flow: row-reverse nowrap;
- justify-content: center;
- }
-
- .rdp-dropdowns {
- margin-top: 2.4rem 3rem 0 3rem;
- width: 21.6rem;
-
- display: flex;
- flex-flow: row-reverse nowrap;
- }
-
- .rdp-dropdown:focus {
- outline: none;
- border: none;
- }
-
- .rdp-years_dropdown {
- width: 11.4rem;
- }
-
- .rdp-months_dropdown {
- width: 9.4rem;
- cursor: not-allowed;
- pointer-events: none;
- }
-
- .rdp-caption_label {
- width: 9.4rem;
- }
- .rdp-caption_label .rdp-chevron {
- visibility: hidden;
- }
-
- .rdp-dropdowns span[role='status']::after {
- content: '년';
- }
-
- .rdp-month_grid {
- border-collapse: separate !important;
-
- border-spacing: 0 1.2rem;
- margin-top: 0.8rem;
-
- width: 43rem;
- padding: 0 1.6rem;
- }
-
- .rdp-weekdays {
- color: ${theme.colors.text03};
- height: 3.2rem;
-
- background-color: ${theme.colors.field02};
- border-radius: 1.2rem;
-
- th {
- ${theme.fonts.label.medium.M13};
- }
-
- th:first-of-type {
- border-top-left-radius: 1.2rem;
- border-bottom-left-radius: 1.2rem;
- }
-
- th:last-of-type {
- border-top-right-radius: 1.2rem;
- border-bottom-right-radius: 1.2rem;
- }
- }
-
- .rdp-day_button {
- width: 4rem;
- height: 4rem;
-
- border-radius: 1.2rem;
- border: none;
-
- margin: 0 auto;
- }
-
- .rdp-selected .rdp-range_middle {
- width: 4rem;
- height: 4rem;
- }
-
- .rdp-today .rdp-day_button {
- color: ${theme.colors.text06};
-
- width: 4rem;
- height: 4rem;
-
- border-radius: 1.2rem;
- border: none;
-
- background-color: ${theme.colors.field02};
- }
-
- .rdp-range_start .rdp-day_button {
- background-color: ${theme.colors.primaryMint};
- color: ${theme.colors.text01};
- }
-
- .rdp-day_button {
- ${theme.fonts.body.normal.M16};
- }
- .rdp-selected {
- ${theme.fonts.body.normal.M16};
- }
-`;
diff --git a/src/app/upload/components/DatePickerForm/DatePickerForm.styles.ts b/src/app/upload/components/DatePickerForm/DatePickerForm.styles.ts
new file mode 100644
index 0000000..21cdc70
--- /dev/null
+++ b/src/app/upload/components/DatePickerForm/DatePickerForm.styles.ts
@@ -0,0 +1,222 @@
+import { css, Theme } from '@emotion/react';
+
+export const datePickerFieldContainer = (theme: Theme) => css`
+ position: relative;
+ outline: none;
+
+ :focus {
+ outline: none;
+ outline: 0.1rem solid ${theme.colors.primaryMint};
+ border-radius: 1.2em;
+
+ .date-picker-field {
+ border: none;
+ }
+ }
+`;
+
+export const datePickerField = (
+ theme: Theme,
+ experimentDateChecked: boolean,
+ isOpen: boolean,
+ isError: boolean,
+) => css`
+ width: 100%;
+ height: 4.8rem;
+
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ padding: 1.3rem 1.6rem;
+
+ border: 0.1rem solid
+ ${isError
+ ? theme.colors.textAlert
+ : experimentDateChecked
+ ? theme.colors.line01
+ : isOpen
+ ? theme.colors.lineTinted
+ : theme.colors.line01};
+
+ border-radius: 1.2rem;
+
+ background-color: ${experimentDateChecked ? theme.colors.field02 : theme.colors.field01};
+
+ cursor: ${experimentDateChecked ? 'not-allowed' : 'pointer'};
+`;
+
+export const placeholderText = (
+ theme: Theme,
+ bothDatesSelected: boolean,
+ experimentDateChecked: boolean,
+) => css`
+ ${theme.fonts.label.large.R14};
+ color: ${experimentDateChecked
+ ? theme.colors.text02
+ : bothDatesSelected
+ ? theme.colors.text06
+ : theme.colors.text02};
+
+ flex: 1;
+
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
+
+export const iconStyle = css`
+ margin-left: 1rem;
+`;
+
+export const popoverLayout = (theme: Theme) => css`
+ background-color: ${theme.colors.field01};
+ border: 0.1rem solid ${theme.colors.line01};
+ border-radius: 8px;
+ padding: 1rem;
+ z-index: ${theme.zIndex.datePickerPopup};
+
+ width: 45.2rem;
+
+ box-shadow: 0rem 0.4rem 1rem rgba(0, 0, 0, 0.1);
+`;
+
+export const datepickerCustom = (theme: Theme) => css`
+ .rdp-months {
+ width: 40rem;
+ position: relative;
+ padding-top: 1.2rem;
+ }
+
+ .rdp-month {
+ display: flex;
+ flex-flow: column nowrap;
+ }
+
+ .rdp-nav {
+ position: absolute;
+ top: 1.2rem;
+ right: 50%;
+ transform: translate(50%);
+
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+ gap: 24.8rem;
+ }
+
+ .rdp-chevron {
+ fill: ${theme.colors.icon03};
+ }
+
+ .rdp-month_caption {
+ display: flex;
+ flex-flow: row-reverse nowrap;
+ justify-content: center;
+ }
+
+ .rdp-dropdowns {
+ margin-top: 2.4rem 3rem 0 3rem;
+ width: 21.6rem;
+
+ display: flex;
+ flex-flow: row-reverse nowrap;
+ }
+
+ .rdp-dropdown:focus {
+ outline: none;
+ border: none;
+ }
+
+ .rdp-years_dropdown {
+ width: 11.4rem;
+ }
+
+ .rdp-months_dropdown {
+ width: 9.4rem;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ .rdp-caption_label {
+ width: 9.4rem;
+ }
+ .rdp-caption_label .rdp-chevron {
+ visibility: hidden;
+ }
+
+ .rdp-dropdowns span[role='status']::after {
+ content: '년';
+ }
+
+ .rdp-month_grid {
+ border-collapse: separate !important;
+
+ border-spacing: 0 1.2rem;
+ margin-top: 0.8rem;
+
+ width: 43rem;
+ padding: 0 1.6rem;
+ }
+
+ .rdp-weekdays {
+ color: ${theme.colors.text03};
+ height: 3.2rem;
+
+ background-color: ${theme.colors.field02};
+ border-radius: 1.2rem;
+
+ th {
+ ${theme.fonts.label.medium.M13};
+ }
+
+ th:first-of-type {
+ border-top-left-radius: 1.2rem;
+ border-bottom-left-radius: 1.2rem;
+ }
+
+ th:last-of-type {
+ border-top-right-radius: 1.2rem;
+ border-bottom-right-radius: 1.2rem;
+ }
+ }
+
+ .rdp-day_button {
+ width: 4rem;
+ height: 4rem;
+
+ border-radius: 1.2rem;
+ border: none;
+
+ margin: 0 auto;
+ }
+
+ .rdp-selected .rdp-range_middle {
+ width: 4rem;
+ height: 4rem;
+ }
+
+ .rdp-today .rdp-day_button {
+ color: ${theme.colors.text06};
+
+ width: 4rem;
+ height: 4rem;
+
+ border-radius: 1.2rem;
+ border: none;
+
+ background-color: ${theme.colors.field02};
+ }
+
+ .rdp-range_start .rdp-day_button {
+ background-color: ${theme.colors.primaryMint};
+ color: ${theme.colors.text01};
+ }
+
+ .rdp-day_button {
+ ${theme.fonts.body.normal.M16};
+ }
+ .rdp-selected {
+ ${theme.fonts.body.normal.M16};
+ }
+`;
diff --git a/src/app/upload/components/DatePickerForm/DatePickerForm.tsx b/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
new file mode 100644
index 0000000..c9869be
--- /dev/null
+++ b/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
@@ -0,0 +1,120 @@
+import * as Popover from '@radix-ui/react-popover';
+import { ko } from 'date-fns/locale';
+import React, { useState } from 'react';
+import { DayPicker, DateRange } from 'react-day-picker';
+import { FieldError } from 'react-hook-form';
+
+import 'react-day-picker/dist/style.css';
+
+import {
+ datepickerCustom,
+ datePickerField,
+ datePickerFieldContainer,
+ iconStyle,
+ placeholderText,
+ popoverLayout,
+} from './DatePickerForm.styles';
+import { formatRange } from '../../upload.utils';
+
+import Icon from '@/components/Icon';
+import { colors } from '@/styles/colors';
+
+interface DatePickerFormProps {
+ placeholder: string;
+ onDateChange: (dates: { from: string | null; to: string | null }) => void;
+ experimentDateChecked?: boolean;
+ error?: FieldError;
+}
+
+const DatePickerField = ({
+ placeholder,
+ onDateChange,
+ experimentDateChecked = false,
+ error,
+}: DatePickerFormProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const [selectedDates, setSelectedDates] = useState({
+ from: undefined,
+ to: undefined,
+ });
+
+ const handleDateChange = (range: DateRange) => {
+ const formattedRange = formatRange(range);
+ setSelectedDates(range);
+ onDateChange(formattedRange);
+ };
+
+ return (
+ {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ setIsOpen((prev) => !prev);
+ }
+ }}
+ >
+
+
+ datePickerField(theme, experimentDateChecked, isOpen, !!error)}
+ aria-label="실험일시 선택"
+ className="date-picker-field"
+ >
+ placeholderText(theme, !!selectedDates.from, experimentDateChecked)}
+ >
+ {!experimentDateChecked
+ ? selectedDates.from
+ ? selectedDates.from?.toLocaleDateString() ===
+ selectedDates.to?.toLocaleDateString()
+ ? `${selectedDates.from?.toLocaleDateString()}`
+ : `${selectedDates.from?.toLocaleDateString()} ~ ${selectedDates.to?.toLocaleDateString()}`
+ : placeholder
+ : '본문 참고'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DatePickerField;
diff --git a/src/app/upload/upload.utils.ts b/src/app/upload/upload.utils.ts
new file mode 100644
index 0000000..3f50031
--- /dev/null
+++ b/src/app/upload/upload.utils.ts
@@ -0,0 +1,10 @@
+import { format } from 'date-fns';
+import { ko } from 'date-fns/locale';
+import { DateRange } from 'react-day-picker';
+
+export const formatRange = (range: DateRange) => {
+ return {
+ from: range.from ? format(range.from, 'yyyy.MM.dd', { locale: ko }) : null,
+ to: range.to ? format(range.to, 'yyyy.MM.dd', { locale: ko }) : null,
+ };
+};
From bbc5dbbf2539a1da98a27cda988f321cd6a7a196 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Wed, 22 Jan 2025 23:18:02 +0900
Subject: [PATCH 05/32] =?UTF-8?q?[YS-172]=20feat:=20InputForm=20=EC=BB=B4?=
=?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../CheckboxWithIcon/CheckboxWithIcon.tsx | 2 +-
.../upload/components/InputForm/InputForm.tsx | 38 +++++++++++++++++++
2 files changed, 39 insertions(+), 1 deletion(-)
create mode 100644 src/app/upload/components/InputForm/InputForm.tsx
diff --git a/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx b/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx
index 001ebca..1a25718 100644
--- a/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx
+++ b/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx
@@ -40,7 +40,7 @@ const CheckboxWithIcon = ({
cursor="pointer"
/>
)}
-
+ {label}
);
diff --git a/src/app/upload/components/InputForm/InputForm.tsx b/src/app/upload/components/InputForm/InputForm.tsx
new file mode 100644
index 0000000..a052030
--- /dev/null
+++ b/src/app/upload/components/InputForm/InputForm.tsx
@@ -0,0 +1,38 @@
+import { input } from '@/app/upload/components/UploadContainer/UploadContainer';
+
+interface InputFormProps {
+ id: string;
+ field: {
+ name: string;
+ value: string;
+ onChange: (event: React.ChangeEvent) => void;
+ onBlur: VoidFunction;
+ };
+ fieldState: {
+ error?: {
+ message?: string;
+ };
+ };
+ placeholder?: string;
+ type?: string;
+}
+
+const InputForm = ({ field, fieldState, placeholder, type = 'text', id }: InputFormProps) => {
+ return (
+ <>
+
+ {fieldState.error && (
+ {fieldState.error.message}
+ )}
+ >
+ );
+};
+
+export default InputForm;
From 0e84ec873ef8f59a85717d65fcc2ed924f375ec6 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Wed, 22 Jan 2025 23:18:24 +0900
Subject: [PATCH 06/32] =?UTF-8?q?[YS-172]=20refactor:=20=EC=97=B0=EA=B5=AC?=
=?UTF-8?q?=20=EC=B1=85=EC=9E=84=EC=9E=90,=20=EC=8B=A4=ED=97=98=EC=9D=BC?=
=?UTF-8?q?=EC=8B=9C=20form=20=EC=97=B0=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../OutlineSection/OutlineSection.tsx | 105 ++++++++++++------
.../upload/hooks/useUploadExperimentPost.tsx | 2 +-
.../upload/uploadExperimentPostSchema.ts | 24 +---
src/types/uploadExperimentPost.ts | 5 +
4 files changed, 77 insertions(+), 59 deletions(-)
create mode 100644 src/types/uploadExperimentPost.ts
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index b98c516..d8684d4 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -1,25 +1,27 @@
import { css, Theme } from '@emotion/react';
import { useState } from 'react';
-import { DateRange } from 'react-day-picker';
+import { Controller, useFormContext, useWatch } from 'react-hook-form';
import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
import CountSelect from '../CountSelect/CountSelect';
import DurationSelect from '../DurationSelect/DurationSelect';
+import InputForm from '../InputForm/InputForm';
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
import RegionPopover from '../RegionPopover/RegionPopover';
import { TextInput } from '../TextInput/TextInput';
import { headingIcon, input, label } from '../UploadContainer/UploadContainer';
-import DatePickerField from '@/app/upload/components/DatePickerField/DatePickerField';
+import DatePickerForm from '@/app/upload/components/DatePickerForm/DatePickerForm';
import { colors } from '@/styles/colors';
-
-export enum MatchType {
- OFFLINE = 'OFFLINE',
- ONLINE = 'ONLINE',
- HYBRID = 'HYBRID',
-}
+import { MatchType } from '@/types/uploadExperimentPost';
const OutlineSection = () => {
+ const { control, setValue, formState } = useFormContext();
+ const formData = useWatch({ control });
+ console.log('formData >> ', formData);
+ console.log('errors>> ', formState.errors);
+
+ // todo useReducer로 리팩토링
const [experimentDateChecked, setExperimentDateChecked] = useState(false);
const [durationChecked, setDurationChecked] = useState(false);
@@ -29,17 +31,6 @@ const OutlineSection = () => {
setSelectedMatchType(method);
};
- // todo react-hook-form 연결 시 controller로 관리할 값
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [selectedDates, setSelectedDates] = useState({
- from: undefined,
- to: undefined,
- });
-
- const handleDateChange = (dates: DateRange) => {
- setSelectedDates(dates);
- };
-
// 실험 장소 지역구 선택
const [selectedRegion, setSelectedRegion] = useState(null);
const [selectedSubRegion, setSelectedSubRegion] = useState(null);
@@ -74,44 +65,84 @@ const OutlineSection = () => {
1실험의 개요를 알려주세요
-
+
);
};
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index 0a46b0e..f66ca2c 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -8,7 +8,7 @@ import UploadExperimentPostSchema, {
const useUploadExperimentPost = () => {
const form = useForm({
mode: 'onBlur',
- reValidateMode: 'onBlur',
+ reValidateMode: 'onChange',
resolver: zodResolver(UploadExperimentPostSchema()),
defaultValues: {
leadResearcher: '',
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index 483735d..11479b5 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -1,6 +1,6 @@
import { z } from 'zod';
-import { MatchType } from '@/app/upload/components/OutlineSection/OutlineSection';
+import { MatchType } from '@/types/uploadExperimentPost';
export type UploadExperimentPostSchemaType = z.infer>;
@@ -22,28 +22,10 @@ const UploadExperimentPostSchema = () => {
// }),
// 실험 시작 날짜
- startDate: z.union(
- [
- z.date({ errorMap: () => ({ message: '' }) }),
- z.null({ errorMap: () => ({ message: '' }) }),
- ],
- {
- required_error: '',
- invalid_type_error: '',
- },
- ),
+ startDate: z.union([z.string(), z.null()]),
// 실험 종료 날짜
- endDate: z.union(
- [
- z.date({ errorMap: () => ({ message: '' }) }),
- z.null({ errorMap: () => ({ message: '' }) }),
- ],
- {
- required_error: '',
- invalid_type_error: '',
- },
- ),
+ endDate: z.union([z.string(), z.null()]),
// 진행 방식
matchType: z.nativeEnum(MatchType),
diff --git a/src/types/uploadExperimentPost.ts b/src/types/uploadExperimentPost.ts
new file mode 100644
index 0000000..f4f65ef
--- /dev/null
+++ b/src/types/uploadExperimentPost.ts
@@ -0,0 +1,5 @@
+export enum MatchType {
+ OFFLINE = 'OFFLINE',
+ ONLINE = 'ONLINE',
+ HYBRID = 'HYBRID',
+}
From eea47531f10a6b0a80ff52af72d481e700bf1208 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Wed, 22 Jan 2025 23:27:53 +0900
Subject: [PATCH 07/32] =?UTF-8?q?[YS-172]=20feat:=20=EC=A7=84=ED=96=89=20?=
=?UTF-8?q?=EB=B0=A9=EC=8B=9D=20RadioButtonGroup=20=EC=BB=B4=ED=8F=AC?=
=?UTF-8?q?=EB=84=8C=ED=8A=B8=20form=20=EC=97=B0=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../OutlineSection/OutlineSection.tsx | 31 +++++++-----
.../RadioButtonGroup.styles.ts | 44 +++++++++++++++++
.../RadioButtonGroup/RadioButtonGroup.tsx | 49 ++++---------------
3 files changed, 71 insertions(+), 53 deletions(-)
create mode 100644 src/app/upload/components/RadioButtonGroup/RadioButtonGroup.styles.ts
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index d8684d4..bf2ca4e 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -25,11 +25,8 @@ const OutlineSection = () => {
const [experimentDateChecked, setExperimentDateChecked] = useState(false);
const [durationChecked, setDurationChecked] = useState(false);
- const [selectedMatchType, setSelectedMatchType] = useState(null);
-
- const handleMatchTypeChange = (method: MatchType) => {
- setSelectedMatchType(method);
- };
+ // 대면 방식
+ const selectedMatchType = useWatch({ control, name: 'matchType' });
// 실험 장소 지역구 선택
const [selectedRegion, setSelectedRegion] = useState(null);
@@ -144,14 +141,22 @@ const OutlineSection = () => {
진행 방식 *
-
- options={[
- { value: MatchType.OFFLINE, label: '대면' },
- { value: MatchType.ONLINE, label: '비대면' },
- { value: MatchType.HYBRID, label: '대면+비대면' },
- ]}
- selectedValue={selectedMatchType}
- onChange={handleMatchTypeChange}
+ (
+
+ options={[
+ { value: MatchType.OFFLINE, label: '대면' },
+ { value: MatchType.ONLINE, label: '비대면' },
+ { value: MatchType.HYBRID, label: '대면+비대면' },
+ ]}
+ selectedValue={field.value}
+ onChange={(value) => field.onChange(value)}
+ isError={!!fieldState.error} // 에러 상태 전달
+ />
+ )}
/>
diff --git a/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.styles.ts b/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.styles.ts
new file mode 100644
index 0000000..0a6cce7
--- /dev/null
+++ b/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.styles.ts
@@ -0,0 +1,44 @@
+import { css, Theme } from '@emotion/react';
+
+export const customRadioGroup = css`
+ display: flex;
+ flex-flow: row nowrap;
+ gap: 0.8rem;
+`;
+
+export const customRadioButton = (theme: Theme) => css`
+ ${theme.fonts.label.large.M14};
+
+ width: 14.533rem;
+ height: 4.8rem;
+
+ padding: 1rem 2rem;
+
+ border: 0.1rem solid ${theme.colors.line01};
+ border-radius: 1.2rem;
+
+ background-color: ${theme.colors.field01};
+
+ cursor: pointer;
+
+ transition: all 0.2s ease;
+
+ &:hover {
+ background-color: ${theme.colors.field02};
+ }
+`;
+
+export const activeRadioButton = (theme: Theme) => css`
+ border: 0.1rem solid ${theme.colors.lineTinted};
+
+ background-color: ${theme.colors.primaryTinted};
+ color: ${theme.colors.textPrimary};
+
+ &:hover {
+ background-color: ${theme.colors.primaryTinted};
+ }
+`;
+
+export const errorRadioButton = (theme: Theme) => css`
+ border-color: ${theme.colors.textAlert} !important;
+`;
diff --git a/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.tsx b/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.tsx
index 1d16b6e..1c5dd02 100644
--- a/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.tsx
+++ b/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.tsx
@@ -1,15 +1,22 @@
-import { css, Theme } from '@emotion/react';
+import {
+ customRadioGroup,
+ customRadioButton,
+ activeRadioButton,
+ errorRadioButton,
+} from './RadioButtonGroup.styles';
interface RadioButtonGroupProps {
options: { value: T; label: string }[];
selectedValue: T | null;
onChange: (value: T) => void;
+ isError?: boolean;
}
const RadioButtonGroup = ({
options,
selectedValue,
onChange,
+ isError = false,
}: RadioButtonGroupProps) => {
return (
@@ -20,6 +27,7 @@ const RadioButtonGroup = ({
css={(theme) => [
customRadioButton(theme),
selectedValue === option.value && activeRadioButton(theme),
+ isError && errorRadioButton(theme),
]}
onClick={() => onChange(option.value)}
>
@@ -31,42 +39,3 @@ const RadioButtonGroup = ({
};
export default RadioButtonGroup;
-
-const customRadioGroup = css`
- display: flex;
- flex-flow: row nowrap;
- gap: 0.8rem;
-`;
-
-const customRadioButton = (theme: Theme) => css`
- ${theme.fonts.label.large.M14};
-
- width: 14.533rem;
- height: 4.8rem;
-
- padding: 1rem 2rem;
-
- border: 0.1rem solid ${theme.colors.line01};
- border-radius: 1.2rem;
-
- background-color: ${theme.colors.field01};
-
- cursor: pointer;
-
- transition: all 0.2s ease;
-
- &:hover {
- background-color: ${theme.colors.field02};
- }
-`;
-
-const activeRadioButton = (theme: Theme) => css`
- border: 0.1rem solid ${theme.colors.lineTinted};
-
- background-color: ${theme.colors.primaryTinted};
- color: ${theme.colors.textPrimary};
-
- &:hover {
- background-color: ${theme.colors.primaryTinted};
- }
-`;
From 3f96b8201996db16ef13f860d105a25c75df0910 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Wed, 22 Jan 2025 23:36:50 +0900
Subject: [PATCH 08/32] =?UTF-8?q?[YS-172]=20feat:=20=EB=B3=B4=EC=83=81=20f?=
=?UTF-8?q?orm=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20InputForm=20=EC=9C=A0?=
=?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=8B=A4=ED=8C=A8=20?=
=?UTF-8?q?=EC=8B=9C=20border-color=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../upload/components/InputForm/InputForm.tsx | 2 +-
.../OutlineSection/OutlineSection.tsx | 17 +++++++++++++++--
.../UploadContainer/UploadContainer.tsx | 4 ++--
src/schema/upload/uploadExperimentPostSchema.ts | 6 +++++-
4 files changed, 23 insertions(+), 6 deletions(-)
diff --git a/src/app/upload/components/InputForm/InputForm.tsx b/src/app/upload/components/InputForm/InputForm.tsx
index a052030..cff8ba5 100644
--- a/src/app/upload/components/InputForm/InputForm.tsx
+++ b/src/app/upload/components/InputForm/InputForm.tsx
@@ -23,7 +23,7 @@ const InputForm = ({ field, fieldState, placeholder, type = 'text', id }: InputF
input(theme, !!fieldState.error)}
type={type}
placeholder={placeholder}
value={field.value || ''}
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index bf2ca4e..3727d0d 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -135,6 +135,7 @@ const OutlineSection = () => {
label="본문 참고"
/>
+
{/* 진행 방식 */}
{/* 실험 장소 */}
diff --git a/src/app/upload/components/UploadContainer/UploadContainer.tsx b/src/app/upload/components/UploadContainer/UploadContainer.tsx
index 2d24dfa..4c8b29d 100644
--- a/src/app/upload/components/UploadContainer/UploadContainer.tsx
+++ b/src/app/upload/components/UploadContainer/UploadContainer.tsx
@@ -144,7 +144,7 @@ export const outlineFormLayout = css`
margin: 0 auto;
`;
-export const input = (theme: Theme) => css`
+export const input = (theme: Theme, isError?: boolean) => css`
${theme.fonts.label.large.R14};
width: 100%;
@@ -152,7 +152,7 @@ export const input = (theme: Theme) => css`
height: 4.8rem;
padding: 10px;
- border: 0.1rem solid ${theme.colors.line01};
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.line01};
border-radius: 1.2rem;
outline: none;
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index 11479b5..1017cc1 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -29,8 +29,10 @@ const UploadExperimentPostSchema = () => {
// 진행 방식
matchType: z.nativeEnum(MatchType),
+
// 실험 횟수
count: z.enum(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']).transform(Number), // 참여 횟수
+
// 소요 시간
timeRequired: z.union([
z.enum([
@@ -60,7 +62,9 @@ const UploadExperimentPostSchema = () => {
// 상세 주소
detailedAddress: z.string().optional(),
// 보상
- reward: z.string().nonempty('보상 필수'),
+ reward: z
+ .string({ message: '최소 3자 이상으로 입력해 주세요' })
+ .min(3, { message: '최소 3자 이상으로 입력해 주세요' }),
// title: z.string().nonempty('실험 제목 필수'), // 실험 제목
// content: z.string().nonempty('실험 본문 필수'), // 실험 본문
From 115a1385593610b0c97da34dc644a574471ff5ed Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Wed, 22 Jan 2025 23:50:40 +0900
Subject: [PATCH 09/32] =?UTF-8?q?[YS-172]=20design:=20SelectInput=20focus?=
=?UTF-8?q?=20=EC=8B=9C=20border-color=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DatePickerForm/DatePickerForm.tsx | 2 +-
.../RadioButtonGroup.styles.ts | 4 ++++
.../RegionPopover/RegionPopover.styles.ts | 10 ++++++++++
.../RegionPopover/RegionPopover.tsx | 20 +++++++++++++++----
.../components/SelectInput/SelectInput.tsx | 4 ++++
5 files changed, 35 insertions(+), 5 deletions(-)
diff --git a/src/app/upload/components/DatePickerForm/DatePickerForm.tsx b/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
index c9869be..3e969d2 100644
--- a/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
+++ b/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
@@ -50,7 +50,7 @@ const DatePickerField = ({
css={datePickerFieldContainer}
tabIndex={0}
onKeyDown={(e) => {
- if (e.key === 'Enter' || e.key === ' ') {
+ if (e.key === ' ') {
e.preventDefault();
setIsOpen((prev) => !prev);
}
diff --git a/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.styles.ts b/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.styles.ts
index 0a6cce7..084048d 100644
--- a/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.styles.ts
+++ b/src/app/upload/components/RadioButtonGroup/RadioButtonGroup.styles.ts
@@ -26,6 +26,10 @@ export const customRadioButton = (theme: Theme) => css`
&:hover {
background-color: ${theme.colors.field02};
}
+
+ :focus {
+ border: 0.1rem solid ${theme.colors.primaryMint};
+ }
`;
export const activeRadioButton = (theme: Theme) => css`
diff --git a/src/app/upload/components/RegionPopover/RegionPopover.styles.ts b/src/app/upload/components/RegionPopover/RegionPopover.styles.ts
index 29102ff..73e9ea0 100644
--- a/src/app/upload/components/RegionPopover/RegionPopover.styles.ts
+++ b/src/app/upload/components/RegionPopover/RegionPopover.styles.ts
@@ -1,5 +1,15 @@
import { css, Theme } from '@emotion/react';
+export const regionPopoverContainer = (theme: Theme) => css`
+ width: 45.2rem;
+
+ :focus {
+ outline: none;
+ border: 0.1rem solid ${theme.colors.primaryMint};
+ border-radius: 1.2rem;
+ }
+`;
+
export const regionField = (theme: Theme) => css`
display: flex;
flex-flow: row nowrap;
diff --git a/src/app/upload/components/RegionPopover/RegionPopover.tsx b/src/app/upload/components/RegionPopover/RegionPopover.tsx
index a8bb469..9b0cf41 100644
--- a/src/app/upload/components/RegionPopover/RegionPopover.tsx
+++ b/src/app/upload/components/RegionPopover/RegionPopover.tsx
@@ -1,7 +1,6 @@
+import { Theme } from '@emotion/react';
import * as Popover from '@radix-ui/react-popover';
-import { css, Theme } from '@emotion/react';
-import { UPLOAD_REGION } from '@/constants/uploadRegion';
-import { input } from '../UploadContainer/UploadContainer';
+
import {
regionField,
popoverContent,
@@ -12,8 +11,12 @@ import {
subRegionList,
subRegionButton,
placeholderText,
+ regionPopoverContainer,
} from './RegionPopover.styles';
+import { input } from '../UploadContainer/UploadContainer';
+
import Icon from '@/components/Icon';
+import { UPLOAD_REGION } from '@/constants/uploadRegion';
interface RegionPopoverProps {
regionPopoverProps: {
@@ -43,7 +46,16 @@ const RegionPopover = ({ regionPopoverProps }: RegionPopoverProps) => {
return (
-
+
{
+ if (e.key === ' ') {
+ e.preventDefault();
+ onOpenRegionPopover(!isOpenRegionPopover);
+ }
+ }}
+ >
diff --git a/src/app/upload/components/SelectInput/SelectInput.tsx b/src/app/upload/components/SelectInput/SelectInput.tsx
index f8a0ce4..bee502c 100644
--- a/src/app/upload/components/SelectInput/SelectInput.tsx
+++ b/src/app/upload/components/SelectInput/SelectInput.tsx
@@ -89,6 +89,10 @@ const selectTrigger = (theme: Theme) => css`
&[data-state='open'] {
border: 0.1rem solid ${theme.colors.primaryMint};
}
+
+ :focus {
+ border: 0.1rem solid ${theme.colors.primaryMint};
+ }
`;
const selectDisabled = (theme: Theme) => css`
From e2c355a9a09150ad95df62a2e128dd84cae98b6d Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Thu, 23 Jan 2025 23:20:21 +0900
Subject: [PATCH 10/32] =?UTF-8?q?[YS-172]=20feat:=20=EC=8B=A4=ED=97=98=20?=
=?UTF-8?q?=EC=9E=A5=EC=86=8C=20form=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=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
---
.../upload/components/InputForm/InputForm.tsx | 74 ++++++++++++++++--
.../OutlineSection/OutlineSection.tsx | 77 +++++++++++++++----
.../RegionPopover/RegionPopover.styles.ts | 5 +-
.../RegionPopover/RegionPopover.tsx | 5 +-
.../upload/components/TextInput/TextInput.tsx | 25 ++++--
.../UploadContainer/UploadContainer.tsx | 2 +-
.../upload/hooks/useUploadExperimentPost.tsx | 15 +++-
.../upload/uploadExperimentPostSchema.ts | 14 ++--
8 files changed, 180 insertions(+), 37 deletions(-)
diff --git a/src/app/upload/components/InputForm/InputForm.tsx b/src/app/upload/components/InputForm/InputForm.tsx
index cff8ba5..11b568f 100644
--- a/src/app/upload/components/InputForm/InputForm.tsx
+++ b/src/app/upload/components/InputForm/InputForm.tsx
@@ -1,3 +1,6 @@
+import { css, Theme } from '@emotion/react';
+import { useState } from 'react';
+
import { input } from '@/app/upload/components/UploadContainer/UploadContainer';
interface InputFormProps {
@@ -15,11 +18,30 @@ interface InputFormProps {
};
placeholder?: string;
type?: string;
+ showErrorMessage?: boolean;
+ size?: 'half' | 'full';
+ maxLength?: number;
}
-const InputForm = ({ field, fieldState, placeholder, type = 'text', id }: InputFormProps) => {
+const InputForm = ({
+ field,
+ fieldState,
+ placeholder,
+ type = 'text',
+ id,
+ showErrorMessage = true,
+ size = 'half',
+ maxLength,
+}: InputFormProps) => {
+ const [textLength, setTextLength] = useState(field.value?.length || 0);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ field.onChange(e);
+ setTextLength(e.target.value.length);
+ };
+
return (
- <>
+
- {fieldState.error && (
-
{fieldState.error.message}
- )}
- >
+
+
+ {maxLength && (
+
+ {textLength}/{maxLength}
+
+ )}
+ {fieldState?.error && showErrorMessage && (
+
{fieldState.error.message}
+ )}
+
+
);
};
export default InputForm;
+
+const textInputContainer = (size: 'half' | 'full') => css`
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ position: relative;
+
+ width: 100%;
+ max-width: ${size === 'half' ? '45.2rem' : '93.6rem'};
+`;
+
+const textCounter = (theme: Theme) => css`
+ ${theme.fonts.label.small.M12};
+ color: ${theme.colors.text02};
+
+ text-align: right;
+`;
+
+const textSubMessageLayout = css`
+ display: flex;
+ flex-flow: row-reverse nowrap;
+ justify-content: space-between;
+ align-items: center;
+`;
+
+const formMessage = (theme: Theme) => css`
+ ${theme.fonts.label.small.M12};
+ color: ${theme.colors.textAlert};
+ margin: 0;
+`;
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index 3727d0d..2b8fa61 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -8,7 +8,6 @@ import DurationSelect from '../DurationSelect/DurationSelect';
import InputForm from '../InputForm/InputForm';
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
import RegionPopover from '../RegionPopover/RegionPopover';
-import { TextInput } from '../TextInput/TextInput';
import { headingIcon, input, label } from '../UploadContainer/UploadContainer';
import DatePickerForm from '@/app/upload/components/DatePickerForm/DatePickerForm';
@@ -36,11 +35,16 @@ const OutlineSection = () => {
const handleRegionSelect = (region: string) => {
setSelectedRegion(region);
setSelectedSubRegion(null);
+
+ setValue('region', region, { shouldValidate: true });
+ setValue('area', '', { shouldValidate: true });
};
const handleSubRegionSelect = (subRegion: string) => {
setSelectedSubRegion(subRegion);
setIsOpenRegionPopover(false);
+
+ setValue('area', subRegion, { shouldValidate: true });
};
const regionPopoverProps = {
@@ -71,18 +75,15 @@ const OutlineSection = () => {
(
- <>
-
- >
+
)}
/>
@@ -145,7 +146,6 @@ const OutlineSection = () => {
(
options={[
@@ -190,11 +190,56 @@ const OutlineSection = () => {
비대면
) : (
-
+ (
+
+ )}
+ />
{/* 지역구 선택 */}
-
+ (
+ (
+
+ )}
+ />
+ )}
+ />
-
+ {/* 상세 주소 입력 */}
+ (
+
+ )}
+ />
)}
diff --git a/src/app/upload/components/RegionPopover/RegionPopover.styles.ts b/src/app/upload/components/RegionPopover/RegionPopover.styles.ts
index 73e9ea0..f7dcf82 100644
--- a/src/app/upload/components/RegionPopover/RegionPopover.styles.ts
+++ b/src/app/upload/components/RegionPopover/RegionPopover.styles.ts
@@ -1,8 +1,11 @@
import { css, Theme } from '@emotion/react';
-export const regionPopoverContainer = (theme: Theme) => css`
+export const regionPopoverContainer = (theme: Theme, isError?: boolean) => css`
width: 45.2rem;
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : 'none'};
+ border-radius: 1.2rem;
+
:focus {
outline: none;
border: 0.1rem solid ${theme.colors.primaryMint};
diff --git a/src/app/upload/components/RegionPopover/RegionPopover.tsx b/src/app/upload/components/RegionPopover/RegionPopover.tsx
index 9b0cf41..5c98246 100644
--- a/src/app/upload/components/RegionPopover/RegionPopover.tsx
+++ b/src/app/upload/components/RegionPopover/RegionPopover.tsx
@@ -1,5 +1,6 @@
import { Theme } from '@emotion/react';
import * as Popover from '@radix-ui/react-popover';
+import { FieldError } from 'react-hook-form';
import {
regionField,
@@ -26,6 +27,7 @@ interface RegionPopoverProps {
onOpenRegionPopover: (open: boolean) => void;
onRegionSelect: (region: string) => void;
onSubRegionSelect: (subRegion: string) => void;
+ error?: FieldError;
};
}
@@ -37,6 +39,7 @@ const RegionPopover = ({ regionPopoverProps }: RegionPopoverProps) => {
onSubRegionSelect,
selectedRegion,
selectedSubRegion,
+ error,
} = regionPopoverProps;
const regionData = selectedRegion
@@ -47,7 +50,7 @@ const RegionPopover = ({ regionPopoverProps }: RegionPopoverProps) => {
regionPopoverContainer(theme, !!error)}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === ' ') {
diff --git a/src/app/upload/components/TextInput/TextInput.tsx b/src/app/upload/components/TextInput/TextInput.tsx
index 536081d..3c16d04 100644
--- a/src/app/upload/components/TextInput/TextInput.tsx
+++ b/src/app/upload/components/TextInput/TextInput.tsx
@@ -5,29 +5,44 @@ interface TextInputProps {
id: string;
placeholder: string;
maxLength?: number;
- message?: string;
- status?: 'error' | '';
size?: 'half' | 'full';
+ field?: {
+ name: string;
+ value: string;
+ onChange: (event: React.ChangeEvent
) => void;
+ onBlur: VoidFunction;
+ };
+ fieldState?: {
+ error?: {
+ message?: string;
+ };
+ };
}
export const TextInput = forwardRef(
- ({ id, placeholder, maxLength, message, status = '', size = 'half' }, ref) => {
+ ({ id, placeholder, maxLength, size = 'half', field, fieldState }, ref) => {
const [textLength, setTextLength] = useState(0);
const handleChange = (e: React.ChangeEvent) => {
+ if (field?.onChange) {
+ field.onChange(e);
+ }
+
setTextLength(e.target.value.length);
};
return (
textInput(theme, status)}
+ value={field?.value || ''}
+ css={(theme) => textInput(theme, fieldState?.error ? 'error' : '')}
/>
{maxLength && (
@@ -35,7 +50,7 @@ export const TextInput = forwardRef(
{textLength}/{maxLength}
)}
- {status === 'error' && message &&
{message}
}
+ {fieldState?.error &&
{fieldState.error.message}
}
);
diff --git a/src/app/upload/components/UploadContainer/UploadContainer.tsx b/src/app/upload/components/UploadContainer/UploadContainer.tsx
index 4c8b29d..dd8b445 100644
--- a/src/app/upload/components/UploadContainer/UploadContainer.tsx
+++ b/src/app/upload/components/UploadContainer/UploadContainer.tsx
@@ -38,7 +38,7 @@ const UploadContainer = () => {
-
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index f66ca2c..770e97c 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -9,17 +9,28 @@ const useUploadExperimentPost = () => {
const form = useForm({
mode: 'onBlur',
reValidateMode: 'onChange',
- resolver: zodResolver(UploadExperimentPostSchema()),
+ resolver: async (data, context, options) => {
+ const matchType = data.matchType;
+ const schema = UploadExperimentPostSchema({ matchType });
+ return zodResolver(schema)(data, context, options);
+ },
defaultValues: {
leadResearcher: '',
startDate: undefined,
endDate: undefined,
+ matchType: undefined,
+ reward: '',
+ univName: undefined,
+ detailedAddress: '',
+ region: undefined,
+ area: undefined,
},
});
const handleSubmit = async (data: UploadExperimentPostSchemaType) => {
try {
- // console.log('공고 등록 form >> ', data);
+ console.log('공고 등록 form >> ', data);
+ // todo region label이 아닌 value로 변경 필요
await form.reset();
} catch (error) {
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index 1017cc1..46612af 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -4,7 +4,10 @@ import { MatchType } from '@/types/uploadExperimentPost';
export type UploadExperimentPostSchemaType = z.infer>;
-const UploadExperimentPostSchema = () => {
+interface UploadExperimentPostSchemaProps {
+ matchType: MatchType;
+}
+const UploadExperimentPostSchema = ({ matchType }: UploadExperimentPostSchemaProps) => {
return z.object({
// targetGroupInfo: z.object({
// startAge: z.number().min(0, '0세 이상'), // 참여 가능 나이 (이상)
@@ -54,13 +57,14 @@ const UploadExperimentPostSchema = () => {
.min(10, { message: '최소 10자 이상으로 입력해 주세요' })
.max(150, { message: '최대 150자 이하로 입력해 주세요' }),
// 대학교
- univName: z.string().nonempty('대학교 이름 필수'),
+ univName: z.string({ required_error: '' }),
// 지역
- region: z.string().nonempty('지역 필수'),
+ region:
+ matchType === MatchType.ONLINE ? z.string().optional() : z.string({ required_error: '' }),
// 지역구
- area: z.string().nonempty('지역구 필수'),
+ area: matchType === MatchType.ONLINE ? z.string().optional() : z.string({ required_error: '' }),
// 상세 주소
- detailedAddress: z.string().optional(),
+ detailedAddress: z.string().max(70, { message: '최대 70자 이하로 입력해 주세요' }),
// 보상
reward: z
.string({ message: '최소 3자 이상으로 입력해 주세요' })
From b067b5bf44b11bc1f43d1d15ae327008a6ac6b68 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Thu, 23 Jan 2025 23:45:27 +0900
Subject: [PATCH 11/32] =?UTF-8?q?[YS-172]=20feat:=20=EC=8B=A4=ED=97=98=20?=
=?UTF-8?q?=EA=B0=9C=EC=9A=94=20form=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=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
---
.../components/CountSelect/CountSelect.tsx | 6 ++-
.../DurationSelect/DurationSelect.tsx | 21 ++++++---
.../upload/components/InputForm/InputForm.tsx | 12 +++---
.../OutlineSection/OutlineSection.tsx | 43 ++++++++++++++-----
.../components/SelectInput/SelectInput.tsx | 8 +++-
.../upload/hooks/useUploadExperimentPost.tsx | 16 ++++++-
.../upload/uploadExperimentPostSchema.ts | 1 +
7 files changed, 80 insertions(+), 27 deletions(-)
diff --git a/src/app/upload/components/CountSelect/CountSelect.tsx b/src/app/upload/components/CountSelect/CountSelect.tsx
index 38941ad..ffb89a3 100644
--- a/src/app/upload/components/CountSelect/CountSelect.tsx
+++ b/src/app/upload/components/CountSelect/CountSelect.tsx
@@ -45,16 +45,18 @@ const countSelectOptions = [
interface CountSelectProps {
value: string | undefined;
- onChange: (value: string) => void;
+ onChange: (value: string | undefined) => void;
+ error: boolean;
}
-const CountSelect = ({ value, onChange }: CountSelectProps) => {
+const CountSelect = ({ value, onChange, error }: CountSelectProps) => {
return (
);
};
diff --git a/src/app/upload/components/DurationSelect/DurationSelect.tsx b/src/app/upload/components/DurationSelect/DurationSelect.tsx
index ac1ef5b..ade78f5 100644
--- a/src/app/upload/components/DurationSelect/DurationSelect.tsx
+++ b/src/app/upload/components/DurationSelect/DurationSelect.tsx
@@ -1,11 +1,5 @@
import SelectInput from '../SelectInput/SelectInput';
-interface DurationSelectProps {
- value: string | undefined;
- onChange: (value: string | undefined) => void;
- referToDetailsChecked?: boolean;
-}
-
export const durationMinutesOptions = [
{ label: '30분 미만', value: 'LESS_30M' },
{ label: '약 30분', value: 'ABOUT_30M' },
@@ -18,7 +12,19 @@ export const durationMinutesOptions = [
{ label: '약 4시간', value: 'ABOUT_4H' },
];
-const DurationSelect = ({ value, onChange, referToDetailsChecked }: DurationSelectProps) => {
+interface DurationSelectProps {
+ value: string | undefined;
+ onChange: (value: string | undefined) => void;
+ referToDetailsChecked?: boolean;
+ error: boolean;
+}
+
+const DurationSelect = ({
+ value,
+ onChange,
+ referToDetailsChecked = false,
+ error,
+}: DurationSelectProps) => {
return (
);
};
diff --git a/src/app/upload/components/InputForm/InputForm.tsx b/src/app/upload/components/InputForm/InputForm.tsx
index 11b568f..0f74002 100644
--- a/src/app/upload/components/InputForm/InputForm.tsx
+++ b/src/app/upload/components/InputForm/InputForm.tsx
@@ -53,15 +53,15 @@ const InputForm = ({
maxLength={maxLength}
/>
-
+
+ {fieldState?.error && showErrorMessage && (
+
{fieldState.error.message}
+ )}
{maxLength && (
{textLength}/{maxLength}
)}
- {fieldState?.error && showErrorMessage && (
-
{fieldState.error.message}
- )}
);
@@ -86,9 +86,9 @@ const textCounter = (theme: Theme) => css`
text-align: right;
`;
-const textSubMessageLayout = css`
+const textSubMessageLayout = (showTextCounter: boolean) => css`
display: flex;
- flex-flow: row-reverse nowrap;
+ flex-flow: ${showTextCounter ? 'row-reverse nowrap' : 'row nowrap'};
justify-content: space-between;
align-items: center;
`;
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index 2b8fa61..1c91732 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -56,10 +56,6 @@ const OutlineSection = () => {
onSubRegionSelect: handleSubRegionSelect,
};
- // 소요 시간
- const [countValue, setCountValue] = useState(undefined);
- const [durationValue, setDurationValue] = useState(undefined);
-
return (
@@ -251,12 +247,39 @@ const OutlineSection = () => {
-
-
+ {/* 실험 횟수 */}
+
+ (
+ field.onChange(value)}
+ error={!!fieldState.error}
+ />
+ )}
+ />
+
+
+ {/* 소요 시간 */}
+
+ (
+ field.onChange(value || null)}
+ referToDetailsChecked={durationChecked}
+ error={!!fieldState.error}
+ />
+ )}
+ />
+
+
+ {/* 본문 참고 체크박스 */}
setDurationChecked((prev) => !prev)}
diff --git a/src/app/upload/components/SelectInput/SelectInput.tsx b/src/app/upload/components/SelectInput/SelectInput.tsx
index bee502c..c6466f9 100644
--- a/src/app/upload/components/SelectInput/SelectInput.tsx
+++ b/src/app/upload/components/SelectInput/SelectInput.tsx
@@ -11,6 +11,7 @@ interface SelectInputProps {
options: { label: string; value: string }[];
placeholder?: string;
referToDetailsChecked?: boolean;
+ error?: boolean;
}
const SelectInput = ({
@@ -19,6 +20,7 @@ const SelectInput = ({
options,
placeholder = '선택',
referToDetailsChecked = false,
+ error = false,
}: SelectInputProps) => {
const [isOpen, setIsOpen] = useState(false);
@@ -30,7 +32,7 @@ const SelectInput = ({
disabled={referToDetailsChecked}
>
@@ -141,3 +143,7 @@ const selectItem = (theme: Theme) => css`
color: ${theme.colors.textPrimary};
}
`;
+
+const selectError = (theme: Theme) => css`
+ border: 0.1rem solid ${theme.colors.textAlert};
+`;
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index 770e97c..da722f8 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -24,6 +24,8 @@ const useUploadExperimentPost = () => {
detailedAddress: '',
region: undefined,
area: undefined,
+ count: undefined,
+ timeRequired: undefined,
},
});
@@ -32,7 +34,19 @@ const useUploadExperimentPost = () => {
console.log('공고 등록 form >> ', data);
// todo region label이 아닌 value로 변경 필요
- await form.reset();
+ await form.reset({
+ leadResearcher: '',
+ startDate: undefined,
+ endDate: undefined,
+ matchType: undefined,
+ reward: '',
+ univName: undefined,
+ detailedAddress: '',
+ region: undefined,
+ area: undefined,
+ count: undefined,
+ timeRequired: undefined,
+ });
} catch (error) {
// console.error('공고 등록 form 저장 중 오류 발생', error);
}
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index 46612af..d6cefaf 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -51,6 +51,7 @@ const UploadExperimentPostSchema = ({ matchType }: UploadExperimentPostSchemaPro
]),
z.null(),
]),
+
// 연구 책임자
leadResearcher: z
.string()
From ba59fcc53b71eee062d4c7696c1d839bdfed348d Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 00:00:08 +0900
Subject: [PATCH 12/32] =?UTF-8?q?[YS-172]=20refactor:=20InputForm=EC=97=90?=
=?UTF-8?q?=20ref=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../upload/components/InputForm/InputForm.tsx | 115 +++++++++++-------
1 file changed, 71 insertions(+), 44 deletions(-)
diff --git a/src/app/upload/components/InputForm/InputForm.tsx b/src/app/upload/components/InputForm/InputForm.tsx
index 0f74002..a4979b2 100644
--- a/src/app/upload/components/InputForm/InputForm.tsx
+++ b/src/app/upload/components/InputForm/InputForm.tsx
@@ -1,7 +1,5 @@
import { css, Theme } from '@emotion/react';
-import { useState } from 'react';
-
-import { input } from '@/app/upload/components/UploadContainer/UploadContainer';
+import React, { forwardRef, useState } from 'react';
interface InputFormProps {
id: string;
@@ -23,49 +21,57 @@ interface InputFormProps {
maxLength?: number;
}
-const InputForm = ({
- field,
- fieldState,
- placeholder,
- type = 'text',
- id,
- showErrorMessage = true,
- size = 'half',
- maxLength,
-}: InputFormProps) => {
- const [textLength, setTextLength] = useState(field.value?.length || 0);
-
- const handleChange = (e: React.ChangeEvent) => {
- field.onChange(e);
- setTextLength(e.target.value.length);
- };
+const InputForm = forwardRef(
+ (
+ {
+ field,
+ fieldState,
+ placeholder,
+ type = 'text',
+ id,
+ showErrorMessage = true,
+ size = 'half',
+ maxLength,
+ },
+ ref,
+ ) => {
+ const [textLength, setTextLength] = useState(field.value?.length || 0);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ field.onChange(e);
+ setTextLength(e.target.value.length);
+ };
+
+ return (
+
+
textInput(theme, fieldState?.error ? 'error' : '')}
+ type={type}
+ placeholder={placeholder}
+ value={field.value || ''}
+ onChange={handleChange}
+ maxLength={maxLength}
+ />
- return (
-
-
input(theme, !!fieldState.error)}
- type={type}
- placeholder={placeholder}
- value={field.value || ''}
- onChange={handleChange}
- maxLength={maxLength}
- />
-
-
- {fieldState?.error && showErrorMessage && (
-
{fieldState.error.message}
- )}
- {maxLength && (
-
- {textLength}/{maxLength}
-
- )}
+
+ {fieldState?.error && showErrorMessage && (
+
{fieldState.error.message}
+ )}
+ {maxLength && (
+
+ {textLength}/{maxLength}
+
+ )}
+
-
- );
-};
+ );
+ },
+);
+
+InputForm.displayName = 'InputForm'; // 컴포넌트 이름 설정
export default InputForm;
@@ -98,3 +104,24 @@ const formMessage = (theme: Theme) => css`
color: ${theme.colors.textAlert};
margin: 0;
`;
+
+const textInput = (theme: Theme, status: string) => css`
+ ${theme.fonts.label.large.R14};
+
+ width: 100%;
+ height: 4.8rem;
+ padding: 0.8rem 1.2rem;
+ border: 0.1rem solid ${status === 'error' ? theme.colors.textAlert : theme.colors.line01};
+ border-radius: 1.2rem;
+ color: ${theme.colors.text06};
+
+ &:focus {
+ border-color: ${status === 'error' ? theme.colors.textAlert : theme.colors.lineTinted};
+ outline: none;
+ }
+
+ &::placeholder {
+ color: ${theme.colors.text02};
+ ${theme.fonts.label.large.R14};
+ }
+`;
From 62458627ea5c0e6483b0b291140de33be2294966 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 00:16:02 +0900
Subject: [PATCH 13/32] =?UTF-8?q?[YS-172]=20feat:=20=EC=8B=A4=ED=97=98=20?=
=?UTF-8?q?=EC=A0=9C=EB=AA=A9=20=EB=B0=8F=20=EB=B3=B8=EB=AC=B8=20form=20?=
=?UTF-8?q?=EC=97=B0=EA=B2=B0=20/=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?=
=?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DescriptionSection/DescriptionSection.tsx | 77 +++++++++++++------
.../OutlineSection/OutlineSection.tsx | 7 +-
.../upload/hooks/useUploadExperimentPost.tsx | 2 +
.../upload/uploadExperimentPostSchema.ts | 12 ++-
4 files changed, 67 insertions(+), 31 deletions(-)
diff --git a/src/app/upload/components/DescriptionSection/DescriptionSection.tsx b/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
index 1a52db8..d2b1a65 100644
--- a/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
+++ b/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
@@ -1,10 +1,13 @@
import { css, Theme } from '@emotion/react';
import Image from 'next/image';
import { useState, ChangeEvent } from 'react';
+import { Controller, useFormContext } from 'react-hook-form';
+import InputForm from '../InputForm/InputForm';
import { headingIcon, input } from '../UploadContainer/UploadContainer';
import Icon from '@/components/Icon';
+import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
import { colors } from '@/styles/colors';
type Photo = {
@@ -14,6 +17,9 @@ type Photo = {
};
const DescriptionSection = () => {
+ const { control, formState } = useFormContext();
+ const contentError = formState.errors.content;
+
const [photos, setPhotos] = useState([]);
const MAX_PHOTOS = 3;
@@ -68,19 +74,38 @@ const DescriptionSection = () => {
-
(
+
+ )}
/>
-
+ {!!contentError &&
{contentError.message}
}
);
};
export default DescriptionSection;
-export const descriptionLayout = css`
- height: 45.4rem;
-`;
+export const descriptionLayout = css``;
export const descriptionFormLayout = css`
width: 100%;
@@ -154,17 +178,15 @@ export const fullInput = css`
max-width: 93.6rem;
`;
-const descriptionContentContainer = (theme: Theme) =>
- css`
- width: 93.6rem;
- border: 0.1rem solid ${theme.colors.line01};
-
- border-radius: 1.2rem;
+const descriptionContentContainer = (theme: Theme, isError: boolean) => css`
+ width: 93.6rem;
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.line01};
+ border-radius: 1.2rem;
- :focus-within {
- border: 0.1rem solid ${theme.colors.lineTinted};
- }
- `;
+ :focus-within {
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.lineTinted};
+ }
+`;
const descriptionTextarea = (photoGridHeight: number) => (theme: Theme) =>
css`
@@ -265,3 +287,10 @@ const deleteButton = css`
border: none;
border-radius: 50%;
`;
+
+const formMessage = (theme: Theme) => css`
+ ${theme.fonts.label.small.M12};
+ color: ${theme.colors.textAlert};
+ margin: 0;
+ padding: 0.8rem 1.6rem;
+`;
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index 1c91732..23113d9 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -15,12 +15,9 @@ import { colors } from '@/styles/colors';
import { MatchType } from '@/types/uploadExperimentPost';
const OutlineSection = () => {
- const { control, setValue, formState } = useFormContext();
- const formData = useWatch({ control });
- console.log('formData >> ', formData);
- console.log('errors>> ', formState.errors);
+ const { control, setValue } = useFormContext();
- // todo useReducer로 리팩토링
+ // todo useReducer로 리팩토링 -> handleSubmit 이후 reset 추가
const [experimentDateChecked, setExperimentDateChecked] = useState(false);
const [durationChecked, setDurationChecked] = useState(false);
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index da722f8..ff2b91f 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -26,6 +26,8 @@ const useUploadExperimentPost = () => {
area: undefined,
count: undefined,
timeRequired: undefined,
+ title: '',
+ content: '',
},
});
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index d6cefaf..5e8902c 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -71,8 +71,16 @@ const UploadExperimentPostSchema = ({ matchType }: UploadExperimentPostSchemaPro
.string({ message: '최소 3자 이상으로 입력해 주세요' })
.min(3, { message: '최소 3자 이상으로 입력해 주세요' }),
- // title: z.string().nonempty('실험 제목 필수'), // 실험 제목
- // content: z.string().nonempty('실험 본문 필수'), // 실험 본문
+ // 실험 제목
+ title: z
+ .string()
+ .min(5, '최소 5자 이상으로 입력해 주세요')
+ .max(70, '최대 70자 이하로 입력해 주세요'),
+ // 실험 본문
+ content: z
+ .string()
+ .min(200, '최소 200자 이상으로 입력해 주세요')
+ .max(5000, '최대 5000자 이하로 입력해 주세요'),
// alarmAgree: z.boolean().default(false), // 알람 동의
});
};
From 5019dba0a66205b22ab4e7601542cc49d706bbd6 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 00:29:22 +0900
Subject: [PATCH 14/32] =?UTF-8?q?[YS-172]=20feat:=20=EC=8B=A4=ED=97=98=20?=
=?UTF-8?q?=EC=B0=B8=EC=97=AC=20=EB=B0=A9=EB=B2=95=20form=20=EC=97=B0?=
=?UTF-8?q?=EA=B2=B0=20=EB=B0=8F=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80?=
=?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ApplyMethodSection/ApplyMethodSection.tsx | 27 ++++++++++++++-----
.../upload/components/InputForm/InputForm.tsx | 6 ++---
.../upload/hooks/useUploadExperimentPost.tsx | 3 +++
.../upload/uploadExperimentPostSchema.ts | 10 +++++++
4 files changed, 37 insertions(+), 9 deletions(-)
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index e288c31..2c39fbd 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -7,6 +7,9 @@ import { TextInput } from '../TextInput/TextInput';
import { headingIcon, label } from '../UploadContainer/UploadContainer';
import { colors } from '@/styles/colors';
+import { Controller, useFormContext } from 'react-hook-form';
+import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
+import InputForm from '../InputForm/InputForm';
enum GenderType {
MALE = 'MALE',
@@ -15,6 +18,9 @@ enum GenderType {
}
const ApplyMethodSection = () => {
+ const { control, formState } = useFormContext();
+ console.log('formState', formState.errors);
+
const [addLink, setAddLink] = useState(false);
const [addContact, setAddContact] = useState(false);
@@ -35,13 +41,22 @@ const ApplyMethodSection = () => {
-
(
+
+ )}
/>
-
{/* 링크 추가 */}
(
/>
- {fieldState?.error && showErrorMessage && (
-
{fieldState.error.message}
- )}
{maxLength && (
{textLength}/{maxLength}
)}
+ {fieldState?.error && showErrorMessage && (
+
{fieldState.error.message}
+ )}
);
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index ff2b91f..cb14c1d 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -28,6 +28,9 @@ const useUploadExperimentPost = () => {
timeRequired: undefined,
title: '',
content: '',
+ applyMethodInfo: {
+ content: '',
+ },
},
});
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index 5e8902c..f7f5e5b 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -81,6 +81,16 @@ const UploadExperimentPostSchema = ({ matchType }: UploadExperimentPostSchemaPro
.string()
.min(200, '최소 200자 이상으로 입력해 주세요')
.max(5000, '최대 5000자 이하로 입력해 주세요'),
+
+ applyMethodInfo: z.object({
+ // 참여 방법
+ content: z
+ .string()
+ .min(5, '최소 5자 이상으로 입력해 주세요')
+ .max(200, '최대 200자 이하로 입력해 주세요'),
+ // formUrl: z.string().url().optional(), // 링크
+ // phoneNum: z.string().optional(), // 연락처
+ }),
// alarmAgree: z.boolean().default(false), // 알람 동의
});
};
From 97cda7f03723e8b87a046451a167b38493b7e8e4 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 08:51:33 +0900
Subject: [PATCH 15/32] =?UTF-8?q?[YS-172]=20design:=20input=20=EC=9E=90?=
=?UTF-8?q?=EB=8F=99=EC=99=84=EC=84=B1=20=EB=B0=B0=EA=B2=BD=EC=83=89=20?=
=?UTF-8?q?=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/styles/reset.ts | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/src/styles/reset.ts b/src/styles/reset.ts
index 3561115..79028f3 100644
--- a/src/styles/reset.ts
+++ b/src/styles/reset.ts
@@ -137,6 +137,26 @@ const reset = css`
height: auto;
resize: none;
}
+
+ input:-webkit-autofill,
+ input:-webkit-autofill:hover,
+ input:-webkit-autofill:focus,
+ input:-webkit-autofill:active {
+ -webkit-text-fill-color: #000;
+ -webkit-box-shadow: 0 0 0px 1000px #fff inset;
+ box-shadow: 0 0 0px 1000px #fff inset;
+ transition: background-color 5000s ease-in-out 0s;
+ }
+
+ input:autofill,
+ input:autofill:hover,
+ input:autofill:focus,
+ input:autofill:active {
+ -webkit-text-fill-color: #000;
+ -webkit-box-shadow: 0 0 0px 1000px #fff inset;
+ box-shadow: 0 0 0px 1000px #fff inset;
+ transition: background-color 5000s ease-in-out 0s;
+ }
`;
export default reset;
From 2a2e639ae9c4ae15bbe276859cb96cb9a47da6ec Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 14:58:38 +0900
Subject: [PATCH 16/32] =?UTF-8?q?[YS-172]=20refactor:=20SelectInput=20Ref?=
=?UTF-8?q?=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../OutlineSection/OutlineSection.tsx | 65 +++---
.../components/SelectInput/SelectInput.tsx | 188 +++++++++---------
src/app/upload/upload.constants.ts | 54 +++++
3 files changed, 184 insertions(+), 123 deletions(-)
create mode 100644 src/app/upload/upload.constants.ts
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index 23113d9..2caf75d 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -3,11 +3,12 @@ import { useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
-import CountSelect from '../CountSelect/CountSelect';
-import DurationSelect from '../DurationSelect/DurationSelect';
+import CountSelect, { countSelectOptions } from '../CountSelect/CountSelect';
+import DurationSelect, { durationMinutesOptions } from '../DurationSelect/DurationSelect';
import InputForm from '../InputForm/InputForm';
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
import RegionPopover from '../RegionPopover/RegionPopover';
+import SelectInput from '../SelectInput/SelectInput';
import { headingIcon, input, label } from '../UploadContainer/UploadContainer';
import DatePickerForm from '@/app/upload/components/DatePickerForm/DatePickerForm';
@@ -249,39 +250,49 @@ const OutlineSection = () => {
(
- field.onChange(value)}
- error={!!fieldState.error}
+
)}
/>
{/* 소요 시간 */}
-
-
(
- field.onChange(value || null)}
- referToDetailsChecked={durationChecked}
- error={!!fieldState.error}
- />
- )}
+ <>
+
+ (
+ field.onChange(value || null),
+ }}
+ fieldState={fieldState}
+ options={durationMinutesOptions}
+ placeholder="1회당 시간 입력"
+ disabled={durationChecked}
+ showErrorMessage={false}
+ />
+ )}
+ />
+
+
+ {/* 본문 참고 체크박스 */}
+ setDurationChecked((prev) => !prev)}
+ label="본문 참고"
/>
-
-
- {/* 본문 참고 체크박스 */}
- setDurationChecked((prev) => !prev)}
- label="본문 참고"
- />
+ >
diff --git a/src/app/upload/components/SelectInput/SelectInput.tsx b/src/app/upload/components/SelectInput/SelectInput.tsx
index c6466f9..ac95aee 100644
--- a/src/app/upload/components/SelectInput/SelectInput.tsx
+++ b/src/app/upload/components/SelectInput/SelectInput.tsx
@@ -1,149 +1,145 @@
import { css, Theme } from '@emotion/react';
import * as Select from '@radix-ui/react-select';
-import { useState } from 'react';
+import React, { forwardRef, useState } from 'react';
import Icon from '@/components/Icon';
import { colors } from '@/styles/colors';
interface SelectInputProps {
- value: string | undefined;
- onChange: (value: string | undefined) => void;
+ field: {
+ name: string;
+ value: string | null;
+ onChange: (value: string) => void;
+ onBlur: VoidFunction;
+ };
+ fieldState: {
+ error?: {
+ message?: string;
+ };
+ };
options: { label: string; value: string }[];
placeholder?: string;
- referToDetailsChecked?: boolean;
- error?: boolean;
+ disabled?: boolean;
+ showErrorMessage?: boolean;
}
-const SelectInput = ({
- value,
- onChange,
- options,
- placeholder = '선택',
- referToDetailsChecked = false,
- error = false,
-}: SelectInputProps) => {
- const [isOpen, setIsOpen] = useState(false);
-
- return (
-
-
-
-
-
-
-
- {!referToDetailsChecked && (
-
-
-
-
- {options.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
-
- )}
-
- );
-};
+const SelectInput = forwardRef(
+ (
+ { field, fieldState, options, placeholder = '선택', disabled = false, showErrorMessage = true },
+ ref,
+ ) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+
+ selectTrigger(theme, disabled, fieldState?.error ? 'error' : '')}
+ onBlur={field?.onBlur}
+ >
+
+
+
+
+
+ {!disabled && (
+
+
+
+
+ {options.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+ )}
+
+ {fieldState?.error && showErrorMessage && (
+
formMessage(theme)}>{fieldState.error.message}
+ )}
+
+ );
+ },
+);
+
+SelectInput.displayName = 'SelectInput';
export default SelectInput;
-const selectTrigger = (theme: Theme) => css`
- ${theme.fonts.label.large.M14};
- color: ${theme.colors.text06};
-
+const selectInputContainer = css`
display: flex;
- align-items: center;
- justify-content: space-between;
+ flex-direction: column;
+ gap: 0.4rem;
+`;
+
+const selectTrigger = (theme: Theme, disabled: boolean, status: string) => css`
+ ${theme.fonts.label.large.R14};
width: 100%;
height: 4.8rem;
-
- padding: 1.3rem 1.6rem;
-
- border: 0.1rem solid ${theme.colors.line01};
+ padding: 0.8rem 1.2rem;
+ border: 0.1rem solid ${status === 'error' ? theme.colors.textAlert : theme.colors.line01};
border-radius: 1.2rem;
+ color: ${disabled ? theme.colors.text02 : theme.colors.text06};
+ background-color: ${disabled ? theme.colors.field02 : 'transparent'};
+
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
&[data-placeholder] {
- ${theme.fonts.label.large.R14};
color: ${theme.colors.text02};
}
- &[data-state='open'] {
- border: 0.1rem solid ${theme.colors.primaryMint};
- }
-
- :focus {
- border: 0.1rem solid ${theme.colors.primaryMint};
+ &:focus {
+ border-color: ${status === 'error' ? theme.colors.textAlert : theme.colors.lineTinted};
+ outline: none;
}
`;
-const selectDisabled = (theme: Theme) => css`
- background-color: ${theme.colors.field02};
- color: ${theme.colors.text02};
- cursor: not-allowed;
- border: 0.1rem solid ${theme.colors.line02};
-`;
-
const selectContent = (theme: Theme) => css`
width: 45.2rem;
- height: 26rem;
-
- padding: 1rem 0.8rem;
-
background-color: ${theme.colors.field01};
border: 0.1rem solid ${theme.colors.line01};
border-radius: 1.2rem;
box-shadow: 0rem 0.4rem 1rem rgba(0, 0, 0, 0.1);
- overflow: hidden;
-
- position: relative;
`;
const selectItem = (theme: Theme) => css`
${theme.fonts.label.large.M14};
-
height: 3.6rem;
-
- border-radius: 1.2rem;
-
padding: 0.7rem 2rem;
-
cursor: pointer;
&[data-highlighted] {
background-color: ${theme.colors.field02};
- border: none;
- outline: none;
}
&[data-state='checked'] {
background-color: ${theme.colors.primaryTinted};
- border: 0.1rem solid ${theme.colors.lineTinted};
color: ${theme.colors.textPrimary};
}
`;
-const selectError = (theme: Theme) => css`
- border: 0.1rem solid ${theme.colors.textAlert};
+const formMessage = (theme: Theme) => css`
+ ${theme.fonts.label.small.M12};
+ color: ${theme.colors.textAlert};
+ margin: 0;
`;
diff --git a/src/app/upload/upload.constants.ts b/src/app/upload/upload.constants.ts
new file mode 100644
index 0000000..5bc0c2e
--- /dev/null
+++ b/src/app/upload/upload.constants.ts
@@ -0,0 +1,54 @@
+export const countSelectOptions = [
+ {
+ label: '1회',
+ value: '1',
+ },
+ {
+ label: '2회',
+ value: '2',
+ },
+ {
+ label: '3회',
+ value: '3',
+ },
+ {
+ label: '4회',
+ value: '4',
+ },
+ {
+ label: '5회',
+ value: '5',
+ },
+ {
+ label: '6회',
+ value: '6',
+ },
+ {
+ label: '7회',
+ value: '7',
+ },
+ {
+ label: '8회',
+ value: '8',
+ },
+ {
+ label: '9회',
+ value: '9',
+ },
+ {
+ label: '10회',
+ value: '10',
+ },
+];
+
+export const durationMinutesOptions = [
+ { label: '30분 미만', value: 'LESS_30M' },
+ { label: '약 30분', value: 'ABOUT_30M' },
+ { label: '약 1시간', value: 'ABOUT_1H' },
+ { label: '약 1시간 30분', value: 'ABOUT_1H30M' },
+ { label: '약 2시간', value: 'ABOUT_2H' },
+ { label: '약 2시간 30분', value: 'ABOUT_2H30M' },
+ { label: '약 3시간', value: 'ABOUT_3H' },
+ { label: '약 3시간 30분', value: 'ABOUT_3H30M' },
+ { label: '약 4시간', value: 'ABOUT_4H' },
+];
From df90fe1fec92b7fbbcb339c7c6ed6210feb2b28e Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 19:48:13 +0900
Subject: [PATCH 17/32] =?UTF-8?q?[YS-172]=20refactor:=20SelectForm=20ref?=
=?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20style=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ApplyMethodSection/ApplyMethodSection.tsx | 10 +--
.../components/CountSelect/CountSelect.tsx | 64 --------------
.../DurationSelect/DurationSelect.tsx | 40 ---------
.../OutlineSection/OutlineSection.tsx | 9 +-
.../SelectForm/SelectForm.styles.ts | 75 +++++++++++++++++
.../SelectForm.tsx} | 83 ++++---------------
.../upload/hooks/useUploadExperimentPost.tsx | 3 +-
7 files changed, 101 insertions(+), 183 deletions(-)
delete mode 100644 src/app/upload/components/CountSelect/CountSelect.tsx
delete mode 100644 src/app/upload/components/DurationSelect/DurationSelect.tsx
create mode 100644 src/app/upload/components/SelectForm/SelectForm.styles.ts
rename src/app/upload/components/{SelectInput/SelectInput.tsx => SelectForm/SelectForm.tsx} (56%)
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index 2c39fbd..18bd6a5 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -1,15 +1,15 @@
import { css, Theme } from '@emotion/react';
import { useState } from 'react';
+import { Controller, useFormContext } from 'react-hook-form';
import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
+import InputForm from '../InputForm/InputForm';
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
import { TextInput } from '../TextInput/TextInput';
import { headingIcon, label } from '../UploadContainer/UploadContainer';
-import { colors } from '@/styles/colors';
-import { Controller, useFormContext } from 'react-hook-form';
import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
-import InputForm from '../InputForm/InputForm';
+import { colors } from '@/styles/colors';
enum GenderType {
MALE = 'MALE',
@@ -18,8 +18,8 @@ enum GenderType {
}
const ApplyMethodSection = () => {
- const { control, formState } = useFormContext();
- console.log('formState', formState.errors);
+ const { control } = useFormContext();
+ // console.log('formState', formState.errors);
const [addLink, setAddLink] = useState(false);
const [addContact, setAddContact] = useState(false);
diff --git a/src/app/upload/components/CountSelect/CountSelect.tsx b/src/app/upload/components/CountSelect/CountSelect.tsx
deleted file mode 100644
index ffb89a3..0000000
--- a/src/app/upload/components/CountSelect/CountSelect.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import SelectInput from '../SelectInput/SelectInput';
-
-const countSelectOptions = [
- {
- label: '1회',
- value: '1',
- },
- {
- label: '2회',
- value: '2',
- },
- {
- label: '3회',
- value: '3',
- },
- {
- label: '4회',
- value: '4',
- },
- {
- label: '5회',
- value: '5',
- },
- {
- label: '6회',
- value: '6',
- },
- {
- label: '7회',
- value: '7',
- },
- {
- label: '8회',
- value: '8',
- },
- {
- label: '9회',
- value: '9',
- },
- {
- label: '10회',
- value: '10',
- },
-];
-
-interface CountSelectProps {
- value: string | undefined;
- onChange: (value: string | undefined) => void;
- error: boolean;
-}
-
-const CountSelect = ({ value, onChange, error }: CountSelectProps) => {
- return (
-
- );
-};
-
-export default CountSelect;
diff --git a/src/app/upload/components/DurationSelect/DurationSelect.tsx b/src/app/upload/components/DurationSelect/DurationSelect.tsx
deleted file mode 100644
index ade78f5..0000000
--- a/src/app/upload/components/DurationSelect/DurationSelect.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import SelectInput from '../SelectInput/SelectInput';
-
-export const durationMinutesOptions = [
- { label: '30분 미만', value: 'LESS_30M' },
- { label: '약 30분', value: 'ABOUT_30M' },
- { label: '약 1시간', value: 'ABOUT_1H' },
- { label: '약 1시간 30분', value: 'ABOUT_1H30M' },
- { label: '약 2시간', value: 'ABOUT_2H' },
- { label: '약 2시간 30분', value: 'ABOUT_2H30M' },
- { label: '약 3시간', value: 'ABOUT_3H' },
- { label: '약 3시간 30분', value: 'ABOUT_3H30M' },
- { label: '약 4시간', value: 'ABOUT_4H' },
-];
-
-interface DurationSelectProps {
- value: string | undefined;
- onChange: (value: string | undefined) => void;
- referToDetailsChecked?: boolean;
- error: boolean;
-}
-
-const DurationSelect = ({
- value,
- onChange,
- referToDetailsChecked = false,
- error,
-}: DurationSelectProps) => {
- return (
-
- );
-};
-
-export default DurationSelect;
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index 2caf75d..ee7433b 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -2,13 +2,12 @@ import { css, Theme } from '@emotion/react';
import { useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
+import { countSelectOptions, durationMinutesOptions } from '../../upload.constants';
import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
-import CountSelect, { countSelectOptions } from '../CountSelect/CountSelect';
-import DurationSelect, { durationMinutesOptions } from '../DurationSelect/DurationSelect';
import InputForm from '../InputForm/InputForm';
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
import RegionPopover from '../RegionPopover/RegionPopover';
-import SelectInput from '../SelectInput/SelectInput';
+import SelectForm from '../SelectForm/SelectForm';
import { headingIcon, input, label } from '../UploadContainer/UploadContainer';
import DatePickerForm from '@/app/upload/components/DatePickerForm/DatePickerForm';
@@ -251,7 +250,7 @@ const OutlineSection = () => {
name="count"
control={control}
render={({ field, fieldState }) => (
- {
name="timeRequired"
control={control}
render={({ field, fieldState }) => (
- css`
+ ${theme.fonts.label.large.R14};
+
+ width: 100%;
+ height: 4.8rem;
+ padding: 0.8rem 1.2rem;
+ border: 0.1rem solid ${status === 'error' ? theme.colors.textAlert : theme.colors.line01};
+ border-radius: 1.2rem;
+ color: ${disabled ? theme.colors.text02 : theme.colors.text06};
+ background-color: ${disabled ? theme.colors.field02 : 'transparent'};
+
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ &[data-placeholder] {
+ color: ${theme.colors.text02};
+ }
+
+ &:focus {
+ border-color: ${status === 'error' ? theme.colors.textAlert : theme.colors.lineTinted};
+ outline: none;
+ }
+`;
+
+export const selectContent = (theme: Theme) => css`
+ width: 45.2rem;
+ background-color: ${theme.colors.field01};
+ border: 0.1rem solid ${theme.colors.line01};
+ border-radius: 1.2rem;
+
+ box-shadow: 0rem 0.4rem 1rem rgba(0, 0, 0, 0.1);
+
+ padding: 0.8rem 0.8rem;
+`;
+
+export const selectItem = (theme: Theme) => css`
+ ${theme.fonts.label.large.M14};
+ height: 3.6rem;
+ padding: 0.7rem 2rem;
+ cursor: pointer;
+ outline: none;
+ margin-bottom: 0.8rem;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+
+ &[data-highlighted] {
+ background-color: ${theme.colors.field02};
+ border-radius: 1.2rem;
+ }
+
+ &[data-state='checked'] {
+ background-color: ${theme.colors.primaryTinted};
+ color: ${theme.colors.textPrimary};
+
+ border-radius: 1.2rem;
+ border: 0.1rem solid ${theme.colors.textPrimary};
+ }
+`;
+
+export const formMessage = (theme: Theme) => css`
+ ${theme.fonts.label.small.M12};
+ color: ${theme.colors.textAlert};
+ margin: 0;
+`;
diff --git a/src/app/upload/components/SelectInput/SelectInput.tsx b/src/app/upload/components/SelectForm/SelectForm.tsx
similarity index 56%
rename from src/app/upload/components/SelectInput/SelectInput.tsx
rename to src/app/upload/components/SelectForm/SelectForm.tsx
index ac95aee..cb6a4f8 100644
--- a/src/app/upload/components/SelectInput/SelectInput.tsx
+++ b/src/app/upload/components/SelectForm/SelectForm.tsx
@@ -1,11 +1,18 @@
-import { css, Theme } from '@emotion/react';
import * as Select from '@radix-ui/react-select';
import React, { forwardRef, useState } from 'react';
+import {
+ formMessage,
+ selectContent,
+ selectInputContainer,
+ selectItem,
+ selectTrigger,
+} from './SelectForm.styles';
+
import Icon from '@/components/Icon';
import { colors } from '@/styles/colors';
-interface SelectInputProps {
+interface SelectFormProps {
field: {
name: string;
value: string | null;
@@ -23,13 +30,15 @@ interface SelectInputProps {
showErrorMessage?: boolean;
}
-const SelectInput = forwardRef(
+const SelectForm = forwardRef(
(
{ field, fieldState, options, placeholder = '선택', disabled = false, showErrorMessage = true },
ref,
) => {
const [isOpen, setIsOpen] = useState(false);
+ if (!options) return null;
+
return (
(
- {options.map((option) => (
+ {options?.map((option) => (
{option.label}
@@ -78,68 +87,6 @@ const SelectInput = forwardRef(
},
);
-SelectInput.displayName = 'SelectInput';
-
-export default SelectInput;
-
-const selectInputContainer = css`
- display: flex;
- flex-direction: column;
- gap: 0.4rem;
-`;
-
-const selectTrigger = (theme: Theme, disabled: boolean, status: string) => css`
- ${theme.fonts.label.large.R14};
-
- width: 100%;
- height: 4.8rem;
- padding: 0.8rem 1.2rem;
- border: 0.1rem solid ${status === 'error' ? theme.colors.textAlert : theme.colors.line01};
- border-radius: 1.2rem;
- color: ${disabled ? theme.colors.text02 : theme.colors.text06};
- background-color: ${disabled ? theme.colors.field02 : 'transparent'};
-
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- &[data-placeholder] {
- color: ${theme.colors.text02};
- }
-
- &:focus {
- border-color: ${status === 'error' ? theme.colors.textAlert : theme.colors.lineTinted};
- outline: none;
- }
-`;
-
-const selectContent = (theme: Theme) => css`
- width: 45.2rem;
- background-color: ${theme.colors.field01};
- border: 0.1rem solid ${theme.colors.line01};
- border-radius: 1.2rem;
-
- box-shadow: 0rem 0.4rem 1rem rgba(0, 0, 0, 0.1);
-`;
-
-const selectItem = (theme: Theme) => css`
- ${theme.fonts.label.large.M14};
- height: 3.6rem;
- padding: 0.7rem 2rem;
- cursor: pointer;
-
- &[data-highlighted] {
- background-color: ${theme.colors.field02};
- }
-
- &[data-state='checked'] {
- background-color: ${theme.colors.primaryTinted};
- color: ${theme.colors.textPrimary};
- }
-`;
+SelectForm.displayName = 'SelectForm';
-const formMessage = (theme: Theme) => css`
- ${theme.fonts.label.small.M12};
- color: ${theme.colors.textAlert};
- margin: 0;
-`;
+export default SelectForm;
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index cb14c1d..11aa53a 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
@@ -36,7 +37,7 @@ const useUploadExperimentPost = () => {
const handleSubmit = async (data: UploadExperimentPostSchemaType) => {
try {
- console.log('공고 등록 form >> ', data);
+ // console.log('공고 등록 form >> ', data);
// todo region label이 아닌 value로 변경 필요
await form.reset({
From 6c9ff08d3c9017ed9a5f98260335b3c4477e52bd Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 22:14:09 +0900
Subject: [PATCH 18/32] =?UTF-8?q?[YS-172]=20feat:=20=EC=97=B0=EB=9D=BD=20?=
=?UTF-8?q?=EB=B0=A9=EB=B2=95=20form=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ApplyMethodSection/ApplyMethodSection.tsx | 67 ++++++++++++++++---
.../upload/components/InputForm/InputForm.tsx | 2 +-
.../UploadContainer/UploadContainer.tsx | 14 +++-
.../upload/hooks/useUploadExperimentPost.tsx | 11 ++-
.../upload/uploadExperimentPostSchema.ts | 21 +++++-
5 files changed, 95 insertions(+), 20 deletions(-)
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index 18bd6a5..99e39c3 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -1,6 +1,6 @@
import { css, Theme } from '@emotion/react';
-import { useState } from 'react';
-import { Controller, useFormContext } from 'react-hook-form';
+import { Dispatch, SetStateAction, useState } from 'react';
+import { Controller, useFormContext, useWatch } from 'react-hook-form';
import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
import InputForm from '../InputForm/InputForm';
@@ -17,12 +17,22 @@ enum GenderType {
ALL = 'ALL',
}
-const ApplyMethodSection = () => {
- const { control } = useFormContext();
- // console.log('formState', formState.errors);
+interface ApplyMethodSectionProps {
+ addLink: boolean;
+ setAddLink: Dispatch>;
+ addContact: boolean;
+ setAddContact: Dispatch>;
+}
- const [addLink, setAddLink] = useState(false);
- const [addContact, setAddContact] = useState(false);
+const ApplyMethodSection = ({
+ addLink,
+ setAddLink,
+ addContact,
+ setAddContact,
+}: ApplyMethodSectionProps) => {
+ const { control, setValue } = useFormContext();
+ const content = useWatch({ control });
+ console.log('content >> ', content);
const [alarmAgree, setAlarmAgree] = useState(false);
@@ -47,11 +57,11 @@ const ApplyMethodSection = () => {
render={({ field, fieldState }) => (
@@ -63,12 +73,30 @@ const ApplyMethodSection = () => {
checked={addLink}
onChange={() => {
setAddLink((prev) => !prev);
+ setValue('applyMethodInfo.formUrl', null);
}}
label="링크를 추가할게요"
align="left"
size="large"
/>
- {addLink && }
+ {addLink && (
+ (
+
+ )}
+ />
+ )}
{/* 연락처 추가 */}
{
align="left"
size="large"
/>
- {addContact && }
+ {addContact && (
+ (
+
+ )}
+ />
+ )}
diff --git a/src/app/upload/components/InputForm/InputForm.tsx b/src/app/upload/components/InputForm/InputForm.tsx
index 16b3c83..e58d24a 100644
--- a/src/app/upload/components/InputForm/InputForm.tsx
+++ b/src/app/upload/components/InputForm/InputForm.tsx
@@ -5,7 +5,7 @@ interface InputFormProps {
id: string;
field: {
name: string;
- value: string;
+ value: string | null;
onChange: (event: React.ChangeEvent) => void;
onBlur: VoidFunction;
};
diff --git a/src/app/upload/components/UploadContainer/UploadContainer.tsx b/src/app/upload/components/UploadContainer/UploadContainer.tsx
index dd8b445..ca1fa57 100644
--- a/src/app/upload/components/UploadContainer/UploadContainer.tsx
+++ b/src/app/upload/components/UploadContainer/UploadContainer.tsx
@@ -3,7 +3,7 @@
import { Theme } from '@emotion/react';
import { css } from '@emotion/react';
import Link from 'next/link';
-import React from 'react';
+import React, { useState } from 'react';
import { FormProvider } from 'react-hook-form';
import useUploadExperimentPost from '../../hooks/useUploadExperimentPost';
@@ -12,7 +12,10 @@ import DescriptionSection from '../DescriptionSection/DescriptionSection';
import OutlineSection from '../OutlineSection/OutlineSection';
const UploadContainer = () => {
- const { form, handleSubmit } = useUploadExperimentPost();
+ const [addLink, setAddLink] = useState(false);
+ const [addContact, setAddContact] = useState(false);
+
+ const { form, handleSubmit } = useUploadExperimentPost({ addLink, addContact });
return (
@@ -30,7 +33,12 @@ const UploadContainer = () => {
{/* 실험 참여 방법 */}
-
+
{/* 버튼 */}
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index 11aa53a..018a5fc 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -6,13 +6,18 @@ import UploadExperimentPostSchema, {
UploadExperimentPostSchemaType,
} from '@/schema/upload/uploadExperimentPostSchema';
-const useUploadExperimentPost = () => {
+interface useUploadExperimentPostProps {
+ addLink: boolean;
+ addContact: boolean;
+}
+
+const useUploadExperimentPost = ({ addLink, addContact }: useUploadExperimentPostProps) => {
const form = useForm({
mode: 'onBlur',
reValidateMode: 'onChange',
resolver: async (data, context, options) => {
const matchType = data.matchType;
- const schema = UploadExperimentPostSchema({ matchType });
+ const schema = UploadExperimentPostSchema({ matchType, addLink, addContact });
return zodResolver(schema)(data, context, options);
},
defaultValues: {
@@ -31,6 +36,8 @@ const useUploadExperimentPost = () => {
content: '',
applyMethodInfo: {
content: '',
+ formUrl: null,
+ phoneNum: null,
},
},
});
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index f7f5e5b..c67db38 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -6,8 +6,14 @@ export type UploadExperimentPostSchemaType = z.infer {
+const UploadExperimentPostSchema = ({
+ matchType,
+ addLink,
+ addContact,
+}: UploadExperimentPostSchemaProps) => {
return z.object({
// targetGroupInfo: z.object({
// startAge: z.number().min(0, '0세 이상'), // 참여 가능 나이 (이상)
@@ -88,8 +94,17 @@ const UploadExperimentPostSchema = ({ matchType }: UploadExperimentPostSchemaPro
.string()
.min(5, '최소 5자 이상으로 입력해 주세요')
.max(200, '최대 200자 이하로 입력해 주세요'),
- // formUrl: z.string().url().optional(), // 링크
- // phoneNum: z.string().optional(), // 연락처
+ // 링크
+ formUrl: addLink
+ ? z
+ .string()
+ .max(100, '최대 100자 이하로 입력해 주세요')
+ .url({ message: '링크를 입력해 주세요' }) // addLink가 true일 때 필수값으로 설정
+ : z.string().nullable(),
+ // 연락처
+ phoneNum: addContact
+ ? z.string().max(100, '최대 100자 이하로 입력해 주세요')
+ : z.string().nullable(),
}),
// alarmAgree: z.boolean().default(false), // 알람 동의
});
From 776629352cd3688ddccb87341c097612689ee2d5 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 22:42:55 +0900
Subject: [PATCH 19/32] =?UTF-8?q?[YS-172]=20feat:=20=EB=82=98=EC=9D=B4=20?=
=?UTF-8?q?=EC=A1=B0=EA=B1=B4=20form=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/upload/components/AgeForm/AgeForm.tsx | 58 +++++++++++++++++++
.../ApplyMethodSection/ApplyMethodSection.tsx | 53 ++++++++---------
.../upload/components/InputForm/InputForm.tsx | 16 ++---
.../upload/hooks/useUploadExperimentPost.tsx | 4 ++
.../upload/uploadExperimentPostSchema.ts | 20 +++----
5 files changed, 106 insertions(+), 45 deletions(-)
create mode 100644 src/app/upload/components/AgeForm/AgeForm.tsx
diff --git a/src/app/upload/components/AgeForm/AgeForm.tsx b/src/app/upload/components/AgeForm/AgeForm.tsx
new file mode 100644
index 0000000..4b3ed13
--- /dev/null
+++ b/src/app/upload/components/AgeForm/AgeForm.tsx
@@ -0,0 +1,58 @@
+import { css, Theme } from '@emotion/react';
+import { ChangeEvent, forwardRef } from 'react';
+
+interface AgeFormProps {
+ id: string;
+ field: {
+ name: string;
+ value: string | number | null;
+ onChange: (event: ChangeEvent) => void;
+ onBlur: VoidFunction;
+ };
+ placeholder?: string;
+}
+
+const AgeForm = forwardRef(({ field, placeholder, id }, ref) => {
+ return (
+
+
+
+ );
+});
+
+AgeForm.displayName = 'AgeForm';
+
+export default AgeForm;
+
+const textInputContainer = () => css`
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ position: relative;
+
+ width: 100%;
+`;
+
+const inputStyle = (theme: Theme) => css`
+ ${theme.fonts.label.large.R14};
+
+ width: 17.2rem;
+ height: 2.2rem;
+
+ text-align: center;
+
+ border: none;
+ outline: none;
+
+ &::placeholder {
+ color: ${theme.colors.text02};
+ }
+`;
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index 99e39c3..b4aae8d 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -2,6 +2,7 @@ import { css, Theme } from '@emotion/react';
import { Dispatch, SetStateAction, useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
+import AgeForm from '../AgeForm/AgeForm';
import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
import InputForm from '../InputForm/InputForm';
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
@@ -11,7 +12,7 @@ import { headingIcon, label } from '../UploadContainer/UploadContainer';
import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
import { colors } from '@/styles/colors';
-enum GenderType {
+export enum GenderType {
MALE = 'MALE',
FEMALE = 'FEMALE',
ALL = 'ALL',
@@ -30,9 +31,14 @@ const ApplyMethodSection = ({
addContact,
setAddContact,
}: ApplyMethodSectionProps) => {
- const { control, setValue } = useFormContext();
+ const { control, setValue, formState } = useFormContext();
const content = useWatch({ control });
console.log('content >> ', content);
+ console.log('error >> ', formState.errors);
+
+ const ageError = !!(
+ formState.errors?.targetGroupInfo?.startAge || formState.errors?.targetGroupInfo?.endAge
+ );
const [alarmAgree, setAlarmAgree] = useState(false);
@@ -138,18 +144,29 @@ const ApplyMethodSection = ({
{/* 나이 */}
-
-
-
{/* 성별 */}
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index 82d10b0..9b919c6 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -42,7 +42,13 @@ const useUploadExperimentPost = ({ addLink, addContact }: useUploadExperimentPos
targetGroupInfo: {
startAge: undefined,
endAge: undefined,
+ genderType: undefined,
+ otherCondition: '',
},
+ imageListInfo: {
+ images: [],
+ },
+ alarmAgree: false,
},
});
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index fdef4f2..f723bd8 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -16,16 +16,6 @@ const UploadExperimentPostSchema = ({
addContact,
}: UploadExperimentPostSchemaProps) => {
return z.object({
- targetGroupInfo: z.object({
- startAge: z.coerce.number().min(0, '0세 이상'), // 참여 가능 나이 (이상)
- endAge: z.coerce.number().min(0, '0세 이상'), // 참여 가능 나이 (이하)
- genderType: z.nativeEnum(GenderType), // 성별
- otherCondition: z.string().optional(), // 기타조건
- }),
- // imageListInfo: z.object({
- // images: z.array(z.string()).optional(), // 이미지 목록 (최대 3장)
- // }),
-
// 실험 시작 날짜
startDate: z.union([z.string(), z.null()]),
@@ -84,6 +74,10 @@ const UploadExperimentPostSchema = ({
.min(200, '최소 200자 이상으로 입력해 주세요')
.max(5000, '최대 5000자 이하로 입력해 주세요'),
+ imageListInfo: z.object({
+ images: z.array(z.string()).optional(), // 이미지 목록 (최대 3장)
+ }),
+
applyMethodInfo: z.object({
// 참여 방법
content: z
@@ -102,7 +96,14 @@ const UploadExperimentPostSchema = ({
? z.string().max(100, '최대 100자 이하로 입력해 주세요')
: z.string().nullable(),
}),
- // alarmAgree: z.boolean().default(false), // 알람 동의
+ targetGroupInfo: z.object({
+ startAge: z.coerce.number().min(0, '0세 이상'), // 참여 가능 나이 (이상)
+ endAge: z.coerce.number().min(0, '0세 이상'), // 참여 가능 나이 (이하)
+ genderType: z.nativeEnum(GenderType), // 성별
+ otherCondition: z.string().optional(), // 기타조건
+ }),
+
+ alarmAgree: z.boolean().default(false), // 알람 동의
});
};
From 8b1f2728a72087ea366e3d331ea869cacf3c7f92 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 22:52:14 +0900
Subject: [PATCH 21/32] =?UTF-8?q?[YS-172]=20feat:=20=EA=B8=B0=ED=83=80=20?=
=?UTF-8?q?=EC=A1=B0=EA=B1=B4=20form=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ApplyMethodSection/ApplyMethodSection.tsx | 25 +++++++++++++------
.../upload/uploadExperimentPostSchema.ts | 2 +-
2 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index 4e426e4..c035c1e 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -6,7 +6,6 @@ import AgeForm from '../AgeForm/AgeForm';
import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
import InputForm from '../InputForm/InputForm';
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
-import { TextInput } from '../TextInput/TextInput';
import { headingIcon, label } from '../UploadContainer/UploadContainer';
import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
@@ -196,13 +195,25 @@ const ApplyMethodSection = ({
{/* 기타 조건 */}
-
기타 조건
+
+ 기타 조건
+
- (
+
+ )}
/>
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index f723bd8..ade7b7a 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -100,7 +100,7 @@ const UploadExperimentPostSchema = ({
startAge: z.coerce.number().min(0, '0세 이상'), // 참여 가능 나이 (이상)
endAge: z.coerce.number().min(0, '0세 이상'), // 참여 가능 나이 (이하)
genderType: z.nativeEnum(GenderType), // 성별
- otherCondition: z.string().optional(), // 기타조건
+ otherCondition: z.string().max(300, '최대 300자 이하로 입력해 주세요').optional(), // 기타조건
}),
alarmAgree: z.boolean().default(false), // 알람 동의
From 4c9e5a93b5c374f644af09668fa882428fdc335f Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 22:56:17 +0900
Subject: [PATCH 22/32] =?UTF-8?q?[YS-172]=20feat:=20=EC=95=8C=EB=A6=BC=20?=
=?UTF-8?q?=EB=8F=99=EC=9D=98=20form=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ApplyMethodSection/ApplyMethodSection.tsx | 24 +++++++++++--------
1 file changed, 14 insertions(+), 10 deletions(-)
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index c035c1e..a629013 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -1,5 +1,5 @@
import { css, Theme } from '@emotion/react';
-import { Dispatch, SetStateAction, useState } from 'react';
+import { Dispatch, SetStateAction } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import AgeForm from '../AgeForm/AgeForm';
@@ -39,8 +39,6 @@ const ApplyMethodSection = ({
formState.errors?.targetGroupInfo?.startAge || formState.errors?.targetGroupInfo?.endAge
);
- const [alarmAgree, setAlarmAgree] = useState(false);
-
return (
{/* 실험 참여 방법 */}
@@ -221,13 +219,19 @@ const ApplyMethodSection = ({
{/* 공고 알림 */}
- setAlarmAgree((prev) => !prev)}
- label="조건에 부합하는 참여자에게 해당 공고를 알릴까요?"
- align="left"
- size="large"
- boldStyle
+ (
+ field.onChange(!field.value)}
+ label="조건에 부합하는 참여자에게 해당 공고를 알릴까요?"
+ align="left"
+ size="large"
+ boldStyle
+ />
+ )}
/>
From 04d6073f3a34051d2d6429d1b36069e98eb8d377 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Fri, 24 Jan 2025 23:10:23 +0900
Subject: [PATCH 23/32] =?UTF-8?q?[YS-172]=20refactor:=20DatePickerForm=20o?=
=?UTF-8?q?nBlur=20=EC=8B=9C=20validate=20=EC=B2=98=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ApplyMethodSection/ApplyMethodSection.tsx | 5 +-
.../DatePickerForm/DatePickerForm.tsx | 207 ++++++++++--------
.../OutlineSection/OutlineSection.tsx | 3 +-
3 files changed, 123 insertions(+), 92 deletions(-)
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index a629013..f98fa8c 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -1,6 +1,6 @@
import { css, Theme } from '@emotion/react';
import { Dispatch, SetStateAction } from 'react';
-import { Controller, useFormContext, useWatch } from 'react-hook-form';
+import { Controller, useFormContext } from 'react-hook-form';
import AgeForm from '../AgeForm/AgeForm';
import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
@@ -31,9 +31,6 @@ const ApplyMethodSection = ({
setAddContact,
}: ApplyMethodSectionProps) => {
const { control, setValue, formState } = useFormContext();
- const content = useWatch({ control });
- console.log('content >> ', content);
- console.log('error >> ', formState.errors);
const ageError = !!(
formState.errors?.targetGroupInfo?.startAge || formState.errors?.targetGroupInfo?.endAge
diff --git a/src/app/upload/components/DatePickerForm/DatePickerForm.tsx b/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
index 3e969d2..6df2a47 100644
--- a/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
+++ b/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
@@ -1,6 +1,6 @@
import * as Popover from '@radix-ui/react-popover';
import { ko } from 'date-fns/locale';
-import React, { useState } from 'react';
+import React, { useState, forwardRef, useEffect } from 'react';
import { DayPicker, DateRange } from 'react-day-picker';
import { FieldError } from 'react-hook-form';
@@ -24,97 +24,130 @@ interface DatePickerFormProps {
onDateChange: (dates: { from: string | null; to: string | null }) => void;
experimentDateChecked?: boolean;
error?: FieldError;
+ field?: {
+ onBlur: VoidFunction;
+ };
}
-const DatePickerField = ({
- placeholder,
- onDateChange,
- experimentDateChecked = false,
- error,
-}: DatePickerFormProps) => {
- const [isOpen, setIsOpen] = useState(false);
-
- const [selectedDates, setSelectedDates] = useState({
- from: undefined,
- to: undefined,
- });
+const DatePickerForm = forwardRef(
+ (
+ { placeholder, onDateChange, experimentDateChecked = false, error, field }: DatePickerFormProps,
+ ref,
+ ) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [selectedDates, setSelectedDates] = useState({
+ from: undefined,
+ to: undefined,
+ });
- const handleDateChange = (range: DateRange) => {
- const formattedRange = formatRange(range);
- setSelectedDates(range);
- onDateChange(formattedRange);
- };
+ const handleDateChange = (range: DateRange) => {
+ const formattedRange = formatRange(range);
+ setSelectedDates(range);
+ onDateChange(formattedRange);
+ };
- return (
- {
- if (e.key === ' ') {
- e.preventDefault();
- setIsOpen((prev) => !prev);
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (event.target instanceof HTMLElement) {
+ if (!event.target.closest('.date-picker-field') && isOpen) {
+ field?.onBlur();
+ setIsOpen(false);
+ }
}
- }}
- >
-
-
- datePickerField(theme, experimentDateChecked, isOpen, !!error)}
- aria-label="실험일시 선택"
- className="date-picker-field"
- >
-
placeholderText(theme, !!selectedDates.from, experimentDateChecked)}
+ };
+
+ document.addEventListener('click', handleClickOutside);
+
+ return () => {
+ document.removeEventListener('click', handleClickOutside);
+ };
+ }, [isOpen, field?.onBlur, field]);
+
+ return (
+ {
+ if (e.key === ' ') {
+ e.preventDefault();
+ setIsOpen((prev) => !prev);
+ }
+ }}
+ >
+
+
+ datePickerField(theme, experimentDateChecked, isOpen, !!error)}
+ aria-label="실험일시 선택"
+ className="date-picker-field"
+ ref={ref}
>
- {!experimentDateChecked
- ? selectedDates.from
- ? selectedDates.from?.toLocaleDateString() ===
- selectedDates.to?.toLocaleDateString()
- ? `${selectedDates.from?.toLocaleDateString()}`
- : `${selectedDates.from?.toLocaleDateString()} ~ ${selectedDates.to?.toLocaleDateString()}`
- : placeholder
- : '본문 참고'}
-
-
-
-
-
-
-
-
- placeholderText(theme, !!selectedDates.from, experimentDateChecked)}
+ >
+ {!experimentDateChecked
+ ? selectedDates.from
+ ? selectedDates.from?.toLocaleDateString() ===
+ selectedDates.to?.toLocaleDateString()
+ ? `${selectedDates.from?.toLocaleDateString()}`
+ : `${selectedDates.from?.toLocaleDateString()} ~ ${selectedDates.to?.toLocaleDateString()}`
+ : placeholder
+ : '본문 참고'}
+
+
+
+
+
+
+
+ {
+ field?.onBlur();
+ setIsOpen(false);
+ }}
+ onInteractOutside={() => {
+ field?.onBlur();
+ setIsOpen(false);
}}
- onSelect={handleDateChange}
- disabled={{ before: new Date() }}
- startMonth={new Date(new Date().getFullYear(), 0)}
- endMonth={new Date(new Date().getFullYear() + 5, 11)}
- required
- css={datepickerCustom}
- captionLayout="dropdown-months"
- showOutsideDays
- />
-
-
-
-
- );
-};
+ >
+
+
+
+
+
+ );
+ },
+);
+
+DatePickerForm.displayName = 'DatePickerForm';
-export default DatePickerField;
+export default DatePickerForm;
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index ee7433b..9d6e30f 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -91,7 +91,7 @@ const OutlineSection = () => {
(
+ render={({ field, fieldState }) => (
{
}}
experimentDateChecked={experimentDateChecked}
error={fieldState.error}
+ field={field}
/>
>
)}
From a5a6a7619cc913e02736c0bf7f0e7e737ed39175 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Sat, 25 Jan 2025 00:05:41 +0900
Subject: [PATCH 24/32] =?UTF-8?q?[YS-172]=20feat:=20=EA=B3=B5=EA=B3=A0=20?=
=?UTF-8?q?=EB=93=B1=EB=A1=9D=20API=20=EC=97=B0=EB=8F=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/apis/useUploadExperimentPostAPI.ts | 77 +++++++++++++++++++
.../ApplyMethodSection/ApplyMethodSection.tsx | 2 +-
.../upload/hooks/useUploadExperimentPost.tsx | 45 ++++++-----
src/app/upload/upload.utils.ts | 28 ++++++-
src/constants/url.ts | 1 +
5 files changed, 129 insertions(+), 24 deletions(-)
create mode 100644 src/apis/useUploadExperimentPostAPI.ts
diff --git a/src/apis/useUploadExperimentPostAPI.ts b/src/apis/useUploadExperimentPostAPI.ts
new file mode 100644
index 0000000..75a7cb1
--- /dev/null
+++ b/src/apis/useUploadExperimentPostAPI.ts
@@ -0,0 +1,77 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { API } from './config';
+
+import { GenderType } from '@/app/upload/components/ApplyMethodSection/ApplyMethodSection';
+import { MatchType } from '@/types/uploadExperimentPost';
+import { API_URL } from '@/constants/url';
+
+interface UseUploadExperimentPostAPIParams {
+ startDate?: string | null;
+ endDate?: string | null;
+ matchType: MatchType;
+ count: number;
+ timeRequired?:
+ | 'LESS_30M'
+ | 'ABOUT_30M'
+ | 'ABOUT_1H'
+ | 'ABOUT_1H30M'
+ | 'ABOUT_2H'
+ | 'ABOUT_2H30M'
+ | 'ABOUT_3H'
+ | 'ABOUT_3H30M'
+ | 'ABOUT_4H'
+ | null;
+ leadResearcher: string;
+ univName?: string;
+ region?: string | undefined;
+ area?: string | undefined;
+ detailedAddress?: string;
+ reward: string;
+ title: string;
+ content: string;
+ imageListInfo?: {
+ images?: string[];
+ };
+ applyMethodInfo: {
+ content: string;
+ formUrl?: string | null;
+ phoneNum?: string | null;
+ };
+ targetGroupInfo: {
+ startAge: number;
+ endAge: number;
+ genderType: GenderType;
+ otherCondition?: string;
+ };
+ alarmAgree: boolean;
+}
+
+interface UploadedPostInfo {
+ experimentPostId: number;
+ title: string;
+ views: number;
+ univName: string;
+ reward: string;
+ durationInfo: {
+ startDate: string;
+ endDate: string;
+ };
+}
+
+interface UseUploadExperimentPostAPIResponse {
+ postInfo: UploadedPostInfo;
+}
+
+const useUploadExperimentPostAPI = () => {
+ const mutationKey = API_URL.uploadPost;
+ const mutationFn = async (data: UseUploadExperimentPostAPIParams) =>
+ await API.post(mutationKey, data).then((res) => res.data);
+
+ return useMutation({
+ mutationKey: [mutationKey],
+ mutationFn,
+ });
+};
+
+export default useUploadExperimentPostAPI;
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index f98fa8c..8c8a405 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -201,7 +201,7 @@ const ApplyMethodSection = ({
{
+ const router = useRouter();
+
const form = useForm({
mode: 'onBlur',
reValidateMode: 'onChange',
@@ -52,27 +57,27 @@ const useUploadExperimentPost = ({ addLink, addContact }: useUploadExperimentPos
},
});
+ console.log('error >> ', form.formState.errors);
+
+ /* 공고 등록 API */
+ const { mutateAsync: uploadExperimentPost } = useUploadExperimentPostAPI();
+
const handleSubmit = async (data: UploadExperimentPostSchemaType) => {
- try {
- // console.log('공고 등록 form >> ', data);
- // todo region label이 아닌 value로 변경 필요
+ const updatedData = {
+ ...data,
+ area: data.area ? convertLabelToValue(data.area) : undefined,
+ };
- await form.reset({
- leadResearcher: '',
- startDate: undefined,
- endDate: undefined,
- matchType: undefined,
- reward: '',
- univName: undefined,
- detailedAddress: '',
- region: undefined,
- area: undefined,
- count: undefined,
- timeRequired: undefined,
- });
- } catch (error) {
- // console.error('공고 등록 form 저장 중 오류 발생', error);
- }
+ uploadExperimentPost(updatedData, {
+ onSuccess: (response) => {
+ form.reset();
+ router.push(`/post/${response.postInfo.experimentPostId}`);
+ console.log('response > ', response);
+ },
+ onError: (error) => {
+ console.error('공고 등록 form 저장 중 오류 발생', error);
+ },
+ });
};
return {
diff --git a/src/app/upload/upload.utils.ts b/src/app/upload/upload.utils.ts
index 3f50031..c6a1ad8 100644
--- a/src/app/upload/upload.utils.ts
+++ b/src/app/upload/upload.utils.ts
@@ -2,9 +2,31 @@ import { format } from 'date-fns';
import { ko } from 'date-fns/locale';
import { DateRange } from 'react-day-picker';
-export const formatRange = (range: DateRange) => {
+import { UPLOAD_REGION } from '@/constants/uploadRegion';
+
+// date 포맷 변환
+const formatRange = (range: DateRange) => {
return {
- from: range.from ? format(range.from, 'yyyy.MM.dd', { locale: ko }) : null,
- to: range.to ? format(range.to, 'yyyy.MM.dd', { locale: ko }) : null,
+ from: range.from ? format(range.from, 'yyyy-MM-dd', { locale: ko }) : null,
+ to: range.to ? format(range.to, 'yyyy-MM-dd', { locale: ko }) : null,
};
};
+
+// area label을 value로 변환하는 함수
+function convertLabelToValue(labelToConvert: string): string {
+ for (const region of UPLOAD_REGION) {
+ if (region.label === labelToConvert) {
+ return region.value;
+ }
+
+ for (const child of region.children || []) {
+ if (child.label === labelToConvert) {
+ return child.value;
+ }
+ }
+ }
+
+ return labelToConvert;
+}
+
+export { formatRange, convertLabelToValue };
diff --git a/src/constants/url.ts b/src/constants/url.ts
index ffc2703..7db7473 100644
--- a/src/constants/url.ts
+++ b/src/constants/url.ts
@@ -8,6 +8,7 @@ export const API_URL = {
join: '/v1/members/signup/researcher',
me: (role: string) => `/v1/members/${role}/me`,
refresh: '/v1/auth/refresh',
+ uploadPost: '/v1/experiment-posts',
};
export const MOCK_API_URL = {
From d038d0fc86805c47041cd56fbe9e676e0977c475 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Sat, 25 Jan 2025 00:23:12 +0900
Subject: [PATCH 25/32] =?UTF-8?q?[YS-172]=20refactor:=20=EC=8B=A4=ED=97=98?=
=?UTF-8?q?=EC=9D=BC=EC=8B=9C=20=EB=B0=8F=20=EC=8B=A4=ED=97=98=EC=9E=A5?=
=?UTF-8?q?=EC=86=8C=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20?=
=?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/apis/useUploadExperimentPostAPI.ts | 6 +--
.../OutlineSection/OutlineSection.tsx | 43 +++++++++++++++----
.../upload/hooks/useUploadExperimentPost.tsx | 3 --
.../upload/uploadExperimentPostSchema.ts | 16 +++----
4 files changed, 44 insertions(+), 24 deletions(-)
diff --git a/src/apis/useUploadExperimentPostAPI.ts b/src/apis/useUploadExperimentPostAPI.ts
index 75a7cb1..6affeec 100644
--- a/src/apis/useUploadExperimentPostAPI.ts
+++ b/src/apis/useUploadExperimentPostAPI.ts
@@ -23,9 +23,9 @@ interface UseUploadExperimentPostAPIParams {
| 'ABOUT_4H'
| null;
leadResearcher: string;
- univName?: string;
- region?: string | undefined;
- area?: string | undefined;
+ univName?: string | null;
+ region?: string | null;
+ area?: string | null;
detailedAddress?: string;
reward: string;
title: string;
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index 9d6e30f..bda0e9f 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -15,9 +15,9 @@ import { colors } from '@/styles/colors';
import { MatchType } from '@/types/uploadExperimentPost';
const OutlineSection = () => {
- const { control, setValue } = useFormContext();
+ const { control, setValue, formState } = useFormContext();
- // todo useReducer로 리팩토링 -> handleSubmit 이후 reset 추가
+ // 실험 일시 및 소요시간 본문 참고 여부
const [experimentDateChecked, setExperimentDateChecked] = useState(false);
const [durationChecked, setDurationChecked] = useState(false);
@@ -29,6 +29,7 @@ const OutlineSection = () => {
const [selectedSubRegion, setSelectedSubRegion] = useState(null);
const [isOpenRegionPopover, setIsOpenRegionPopover] = useState(false);
+ // 유저 선택에 따라 재검증 및 null 처리
const handleRegionSelect = (region: string) => {
setSelectedRegion(region);
setSelectedSubRegion(null);
@@ -44,6 +45,31 @@ const OutlineSection = () => {
setValue('area', subRegion, { shouldValidate: true });
};
+ const handleMatchTypeChange = (value: MatchType) => {
+ setValue('matchType', value, { shouldValidate: true });
+
+ if (value === MatchType.ONLINE) {
+ setValue('region', null, { shouldValidate: true });
+ setValue('area', null, { shouldValidate: true });
+ setValue('univName', null, { shouldValidate: true });
+ } else {
+ setValue('region', '', { shouldValidate: true });
+ setValue('area', '', { shouldValidate: true });
+ setValue('univName', '', { shouldValidate: true });
+ }
+ };
+
+ const handleDurationCheckboxChange = () => {
+ const newCheckedState = !durationChecked;
+ setDurationChecked(newCheckedState);
+
+ if (newCheckedState) {
+ setValue('timeRequired', null, { shouldValidate: true });
+ } else {
+ setValue('timeRequired', '', { shouldValidate: true });
+ }
+ };
+
const regionPopoverProps = {
isOpenRegionPopover,
onOpenRegionPopover: setIsOpenRegionPopover,
@@ -148,7 +174,10 @@ const OutlineSection = () => {
{ value: MatchType.HYBRID, label: '대면+비대면' },
]}
selectedValue={field.value}
- onChange={(value) => field.onChange(value)}
+ onChange={(value) => {
+ field.onChange(value);
+ handleMatchTypeChange(value);
+ }}
isError={!!fieldState.error}
/>
)}
@@ -271,11 +300,7 @@ const OutlineSection = () => {
control={control}
render={({ field, fieldState }) => (
field.onChange(value || null),
- }}
+ field={field}
fieldState={fieldState}
options={durationMinutesOptions}
placeholder="1회당 시간 입력"
@@ -289,7 +314,7 @@ const OutlineSection = () => {
{/* 본문 참고 체크박스 */}
setDurationChecked((prev) => !prev)}
+ onChange={handleDurationCheckboxChange}
label="본문 참고"
/>
>
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index 944ffad..01817f5 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -57,8 +57,6 @@ const useUploadExperimentPost = ({ addLink, addContact }: useUploadExperimentPos
},
});
- console.log('error >> ', form.formState.errors);
-
/* 공고 등록 API */
const { mutateAsync: uploadExperimentPost } = useUploadExperimentPostAPI();
@@ -72,7 +70,6 @@ const useUploadExperimentPost = ({ addLink, addContact }: useUploadExperimentPos
onSuccess: (response) => {
form.reset();
router.push(`/post/${response.postInfo.experimentPostId}`);
- console.log('response > ', response);
},
onError: (error) => {
console.error('공고 등록 form 저장 중 오류 발생', error);
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index ade7b7a..f655be0 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -29,8 +29,8 @@ const UploadExperimentPostSchema = ({
count: z.enum(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']).transform(Number), // 참여 횟수
// 소요 시간
- timeRequired: z.union([
- z.enum([
+ timeRequired: z
+ .enum([
'LESS_30M',
'ABOUT_30M',
'ABOUT_1H',
@@ -40,9 +40,8 @@ const UploadExperimentPostSchema = ({
'ABOUT_3H',
'ABOUT_3H30M',
'ABOUT_4H',
- ]),
- z.null(),
- ]),
+ ])
+ .nullable(),
// 연구 책임자
leadResearcher: z
@@ -50,12 +49,11 @@ const UploadExperimentPostSchema = ({
.min(10, { message: '최소 10자 이상으로 입력해 주세요' })
.max(150, { message: '최대 150자 이하로 입력해 주세요' }),
// 대학교
- univName: z.string({ required_error: '' }),
+ univName: z.string().min(1, '').nullable(),
// 지역
- region:
- matchType === MatchType.ONLINE ? z.string().optional() : z.string({ required_error: '' }),
+ region: z.string().min(1, '').nullable(),
// 지역구
- area: matchType === MatchType.ONLINE ? z.string().optional() : z.string({ required_error: '' }),
+ area: z.string().min(1, '').nullable(),
// 상세 주소
detailedAddress: z.string().max(70, { message: '최대 70자 이하로 입력해 주세요' }),
// 보상
From 8d2c115b1714a242642961efa095380d3809c50d Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Sat, 25 Jan 2025 01:33:45 +0900
Subject: [PATCH 26/32] =?UTF-8?q?[YS-172]=20refactor:=20=EC=A7=80=EC=97=AD?=
=?UTF-8?q?=EA=B5=AC=20blur=20=EC=8B=9C=20validate=20=EC=B2=98=EB=A6=AC=20?=
=?UTF-8?q?=EB=B0=8F=20=EA=B7=B8=EC=97=90=20=EB=94=B0=EB=A5=B8=20border=20?=
=?UTF-8?q?=EC=83=89=EC=83=81=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DatePickerForm/DatePickerForm.tsx | 19 +-------
.../OutlineSection/OutlineSection.tsx | 3 +-
.../RegionPopover/RegionPopover.styles.ts | 16 +++----
.../RegionPopover/RegionPopover.tsx | 45 +++++++++++++++----
.../components/SelectForm/SelectForm.tsx | 2 +-
5 files changed, 46 insertions(+), 39 deletions(-)
diff --git a/src/app/upload/components/DatePickerForm/DatePickerForm.tsx b/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
index 6df2a47..5354b91 100644
--- a/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
+++ b/src/app/upload/components/DatePickerForm/DatePickerForm.tsx
@@ -1,6 +1,6 @@
import * as Popover from '@radix-ui/react-popover';
import { ko } from 'date-fns/locale';
-import React, { useState, forwardRef, useEffect } from 'react';
+import React, { useState, forwardRef } from 'react';
import { DayPicker, DateRange } from 'react-day-picker';
import { FieldError } from 'react-hook-form';
@@ -46,23 +46,6 @@ const DatePickerForm = forwardRef(
onDateChange(formattedRange);
};
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (event.target instanceof HTMLElement) {
- if (!event.target.closest('.date-picker-field') && isOpen) {
- field?.onBlur();
- setIsOpen(false);
- }
- }
- };
-
- document.addEventListener('click', handleClickOutside);
-
- return () => {
- document.removeEventListener('click', handleClickOutside);
- };
- }, [isOpen, field?.onBlur, field]);
-
return (
{
name="region"
control={control}
rules={{ required: '지역을 선택해 주세요' }}
- render={({ fieldState }) => (
+ render={({ fieldState, field }) => (
{
regionPopoverProps={{
...regionPopoverProps,
error: fieldState.error || areaFieldState.error,
+ field,
}}
/>
)}
diff --git a/src/app/upload/components/RegionPopover/RegionPopover.styles.ts b/src/app/upload/components/RegionPopover/RegionPopover.styles.ts
index f7dcf82..110b216 100644
--- a/src/app/upload/components/RegionPopover/RegionPopover.styles.ts
+++ b/src/app/upload/components/RegionPopover/RegionPopover.styles.ts
@@ -1,15 +1,14 @@
import { css, Theme } from '@emotion/react';
-export const regionPopoverContainer = (theme: Theme, isError?: boolean) => css`
+export const regionPopoverContainer = (theme: Theme, isError: boolean) => css`
width: 45.2rem;
-
- border: 0.1rem solid ${isError ? theme.colors.textAlert : 'none'};
border-radius: 1.2rem;
+ outline: none;
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : 'none'};
- :focus {
+ &[data-state='open'] {
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.primaryMint};
outline: none;
- border: 0.1rem solid ${theme.colors.primaryMint};
- border-radius: 1.2rem;
}
`;
@@ -18,10 +17,7 @@ export const regionField = (theme: Theme) => css`
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
-
- &[data-state='open'] {
- border: 0.1rem solid ${theme.colors.primaryMint};
- }
+ border-radius: 1.2rem;
`;
export const placeholderText = (theme: Theme, hasValue: boolean) => css`
diff --git a/src/app/upload/components/RegionPopover/RegionPopover.tsx b/src/app/upload/components/RegionPopover/RegionPopover.tsx
index 5c98246..f2099f5 100644
--- a/src/app/upload/components/RegionPopover/RegionPopover.tsx
+++ b/src/app/upload/components/RegionPopover/RegionPopover.tsx
@@ -1,6 +1,6 @@
-import { Theme } from '@emotion/react';
import * as Popover from '@radix-ui/react-popover';
import { FieldError } from 'react-hook-form';
+import { useRef } from 'react';
import {
regionField,
@@ -28,6 +28,9 @@ interface RegionPopoverProps {
onRegionSelect: (region: string) => void;
onSubRegionSelect: (subRegion: string) => void;
error?: FieldError;
+ field?: {
+ onBlur: VoidFunction;
+ };
};
}
@@ -40,17 +43,34 @@ const RegionPopover = ({ regionPopoverProps }: RegionPopoverProps) => {
selectedRegion,
selectedSubRegion,
error,
+ field,
} = regionPopoverProps;
const regionData = selectedRegion
? UPLOAD_REGION.find((region) => region.value === selectedRegion) || null
: UPLOAD_REGION.find((region) => region.value === 'SEOUL') || null;
+ const popoverRef = useRef(null);
+
+ const handleSubRegionSelect = (subRegionLabel: string) => {
+ const selectedRegion = UPLOAD_REGION.find((region) =>
+ region.children.some((subRegion) => subRegion.label === subRegionLabel),
+ );
+
+ if (selectedRegion) {
+ onRegionSelect(selectedRegion.value);
+ }
+
+ onSubRegionSelect(subRegionLabel);
+ };
+
return (
regionPopoverContainer(theme, !!error)}
+ className="region-field"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === ' ') {
@@ -59,12 +79,8 @@ const RegionPopover = ({ regionPopoverProps }: RegionPopoverProps) => {
}
}}
>
-
-
- placeholderText(theme, !!(selectedRegion && selectedSubRegion))
- }
- >
+ [input, regionField(theme)]}>
+
placeholderText(theme, !!(selectedRegion && selectedSubRegion))}>
{selectedRegion && selectedSubRegion
? `${regionData?.label} ${selectedSubRegion}`
: '지역구 선택'}
@@ -77,7 +93,18 @@ const RegionPopover = ({ regionPopoverProps }: RegionPopoverProps) => {
-
+ {
+ field?.onBlur();
+ onOpenRegionPopover(false);
+ }}
+ onInteractOutside={() => {
+ field?.onBlur();
+ onOpenRegionPopover(false);
+ }}
+ >
{/* 지역 */}
@@ -101,7 +128,7 @@ const RegionPopover = ({ regionPopoverProps }: RegionPopoverProps) => {
subRegionButton,
selectedSubRegion === subRegion.label && activeRegionButton,
]}
- onClick={() => onSubRegionSelect(subRegion.label)}
+ onClick={() => handleSubRegionSelect(subRegion.label)}
>
{subRegion.label}
diff --git a/src/app/upload/components/SelectForm/SelectForm.tsx b/src/app/upload/components/SelectForm/SelectForm.tsx
index cb6a4f8..74a8b9c 100644
--- a/src/app/upload/components/SelectForm/SelectForm.tsx
+++ b/src/app/upload/components/SelectForm/SelectForm.tsx
@@ -52,7 +52,7 @@ const SelectForm = forwardRef
(
css={(theme) => selectTrigger(theme, disabled, fieldState?.error ? 'error' : '')}
onBlur={field?.onBlur}
>
-
+
Date: Sat, 25 Jan 2025 02:32:55 +0900
Subject: [PATCH 27/32] =?UTF-8?q?[YS-172]=20refactor:=20=EC=8A=A4=ED=83=80?=
=?UTF-8?q?=EC=9D=BC=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F?=
=?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20?=
=?UTF-8?q?=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../{ => hooks}/useUploadExperimentPostAPI.ts | 0
.../components/AgeForm/AgeForm.styles.ts | 26 +++
src/app/upload/components/AgeForm/AgeForm.tsx | 27 +---
.../ApplyMethodSection.styles.ts | 61 +++++++
.../ApplyMethodSection/ApplyMethodSection.tsx | 76 ++-------
.../CheckboxWithIcon.styles.ts | 34 ++++
.../CheckboxWithIcon/CheckboxWithIcon.tsx | 34 +---
.../DescriptionSection.styles.ts | 129 +++++++++++++++
.../DescriptionSection/DescriptionSection.tsx | 148 ++---------------
.../components/InputForm/InputForm.styles.ts | 52 ++++++
.../upload/components/InputForm/InputForm.tsx | 59 +------
.../OutlineSection/OutlineSection.styles.ts | 67 ++++++++
.../OutlineSection/OutlineSection.tsx | 78 ++-------
.../RegionPopover/RegionPopover.tsx | 2 +-
.../upload/components/TextInput/TextInput.tsx | 111 -------------
.../UploadContainer/UploadContainer.styles.ts | 141 ++++++++++++++++
.../UploadContainer/UploadContainer.tsx | 150 +-----------------
.../upload/hooks/useUploadExperimentPost.tsx | 2 +-
18 files changed, 565 insertions(+), 632 deletions(-)
rename src/apis/{ => hooks}/useUploadExperimentPostAPI.ts (100%)
create mode 100644 src/app/upload/components/AgeForm/AgeForm.styles.ts
create mode 100644 src/app/upload/components/ApplyMethodSection/ApplyMethodSection.styles.ts
create mode 100644 src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.styles.ts
create mode 100644 src/app/upload/components/DescriptionSection/DescriptionSection.styles.ts
create mode 100644 src/app/upload/components/InputForm/InputForm.styles.ts
create mode 100644 src/app/upload/components/OutlineSection/OutlineSection.styles.ts
delete mode 100644 src/app/upload/components/TextInput/TextInput.tsx
create mode 100644 src/app/upload/components/UploadContainer/UploadContainer.styles.ts
diff --git a/src/apis/useUploadExperimentPostAPI.ts b/src/apis/hooks/useUploadExperimentPostAPI.ts
similarity index 100%
rename from src/apis/useUploadExperimentPostAPI.ts
rename to src/apis/hooks/useUploadExperimentPostAPI.ts
diff --git a/src/app/upload/components/AgeForm/AgeForm.styles.ts b/src/app/upload/components/AgeForm/AgeForm.styles.ts
new file mode 100644
index 0000000..948130b
--- /dev/null
+++ b/src/app/upload/components/AgeForm/AgeForm.styles.ts
@@ -0,0 +1,26 @@
+import { css, Theme } from '@emotion/react';
+
+export const textInputContainer = () => css`
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ position: relative;
+
+ width: 100%;
+`;
+
+export const inputStyle = (theme: Theme) => css`
+ ${theme.fonts.label.large.R14};
+
+ width: 17.2rem;
+ height: 2.2rem;
+
+ text-align: center;
+
+ border: none;
+ outline: none;
+
+ &::placeholder {
+ color: ${theme.colors.text02};
+ }
+`;
diff --git a/src/app/upload/components/AgeForm/AgeForm.tsx b/src/app/upload/components/AgeForm/AgeForm.tsx
index 4b3ed13..7e115b9 100644
--- a/src/app/upload/components/AgeForm/AgeForm.tsx
+++ b/src/app/upload/components/AgeForm/AgeForm.tsx
@@ -1,5 +1,5 @@
-import { css, Theme } from '@emotion/react';
import { ChangeEvent, forwardRef } from 'react';
+import { inputStyle, textInputContainer } from './AgeForm.styles';
interface AgeFormProps {
id: string;
@@ -31,28 +31,3 @@ const AgeForm = forwardRef(({ field, placeholder
AgeForm.displayName = 'AgeForm';
export default AgeForm;
-
-const textInputContainer = () => css`
- display: flex;
- flex-direction: column;
- gap: 0.4rem;
- position: relative;
-
- width: 100%;
-`;
-
-const inputStyle = (theme: Theme) => css`
- ${theme.fonts.label.large.R14};
-
- width: 17.2rem;
- height: 2.2rem;
-
- text-align: center;
-
- border: none;
- outline: none;
-
- &::placeholder {
- color: ${theme.colors.text02};
- }
-`;
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.styles.ts b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.styles.ts
new file mode 100644
index 0000000..16f3260
--- /dev/null
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.styles.ts
@@ -0,0 +1,61 @@
+import { css, Theme } from '@emotion/react';
+
+export const applyMethodContainer = css`
+ margin-top: 2rem;
+ margin-bottom: 4.8rem;
+`;
+
+export const applyMethodContentLayout = css`
+ display: flex;
+ flex-flow: column nowrap;
+`;
+
+export const addContactInfoContainer = css`
+ width: 100%;
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 0.8rem;
+`;
+
+export const targetConditionLayout = css`
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 2.8rem;
+`;
+
+export const targetGroupContainer = css`
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+`;
+
+export const ageInputContainer = (theme: Theme, isError: boolean) => css`
+ width: 45.2rem;
+ height: 4.8rem;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.line01};
+ border-radius: 1.2rem;
+ padding: 1.3rem 1.6rem;
+`;
+
+export const textStyle = (theme: Theme) => css`
+ ${theme.fonts.label.large.M14};
+ color: ${theme.colors.text06};
+`;
+
+export const alarmAgreeContainer = (theme: Theme) => css`
+ width: fit-content;
+ height: 3.4rem;
+
+ padding: 0 1rem;
+
+ background-color: ${theme.colors.field02};
+ border-radius: 0.8rem;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index 8c8a405..8db1214 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -6,10 +6,20 @@ import AgeForm from '../AgeForm/AgeForm';
import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
import InputForm from '../InputForm/InputForm';
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
-import { headingIcon, label } from '../UploadContainer/UploadContainer';
import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
import { colors } from '@/styles/colors';
+import { headingIcon, label } from '../UploadContainer/UploadContainer.styles';
+import {
+ addContactInfoContainer,
+ ageInputContainer,
+ alarmAgreeContainer,
+ applyMethodContainer,
+ applyMethodContentLayout,
+ targetConditionLayout,
+ targetGroupContainer,
+ textStyle,
+} from './ApplyMethodSection.styles';
export enum GenderType {
MALE = 'MALE',
@@ -37,7 +47,7 @@ const ApplyMethodSection = ({
);
return (
-
+
{/* 실험 참여 방법 */}
@@ -236,65 +246,3 @@ const ApplyMethodSection = ({
};
export default ApplyMethodSection;
-
-export const applyMethodLayout = css``;
-
-const applyMethodContainer = css`
- margin-top: 2rem;
- margin-bottom: 4.8rem;
-`;
-
-const applyMethodContentLayout = css`
- display: flex;
- flex-flow: column nowrap;
-`;
-
-const addContactInfoContainer = css`
- width: 100%;
- display: flex;
- flex-flow: column nowrap;
- gap: 0.8rem;
-`;
-
-const targetConditionLayout = css`
- display: flex;
- flex-flow: column nowrap;
- gap: 2.8rem;
-`;
-
-const targetGroupContainer = css`
- display: flex;
- flex-flow: row nowrap;
- justify-content: space-between;
-`;
-
-const ageInputContainer = (theme: Theme, isError: boolean) => css`
- width: 45.2rem;
- height: 4.8rem;
-
- display: flex;
- align-items: center;
- justify-content: center;
- border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.line01};
- border-radius: 1.2rem;
- padding: 1.3rem 1.6rem;
-`;
-
-const textStyle = (theme: Theme) => css`
- ${theme.fonts.label.large.M14};
- color: ${theme.colors.text06};
-`;
-
-const alarmAgreeContainer = (theme: Theme) => css`
- width: fit-content;
- height: 3.4rem;
-
- padding: 0 1rem;
-
- background-color: ${theme.colors.field02};
- border-radius: 0.8rem;
-
- display: flex;
- justify-content: center;
- align-items: center;
-`;
diff --git a/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.styles.ts b/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.styles.ts
new file mode 100644
index 0000000..7cd7772
--- /dev/null
+++ b/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.styles.ts
@@ -0,0 +1,34 @@
+import { css, Theme } from '@emotion/react';
+
+export const checkboxLayout = (
+ theme: Theme,
+ align: 'left' | 'center' | 'right',
+ size: 'small' | 'large',
+ boldStyle: boolean,
+) => css`
+ ${size === 'small' ? theme.fonts.label.small.M12 : theme.fonts.label.large.R14};
+
+ font-weight: ${boldStyle || size === 'small' ? '500' : '400'};
+
+ color: ${boldStyle ? theme.colors.text06 : theme.colors.text04};
+
+ margin-top: 0.4rem;
+
+ display: flex;
+ justify-content: ${align};
+
+ cursor: pointer;
+
+ label {
+ cursor: pointer;
+ }
+`;
+
+export const checkboxContainer = (size: 'small' | 'large') => css`
+ width: fit-content;
+
+ display: flex;
+ flex-flow: row nowrap;
+ gap: ${size === 'small' ? '0.2rem' : '0.4rem'};
+ align-items: center;
+`;
diff --git a/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx b/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx
index 1a25718..76fe820 100644
--- a/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx
+++ b/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx
@@ -2,6 +2,7 @@ import { css, Theme } from '@emotion/react';
import Icon from '@/components/Icon';
import { colors } from '@/styles/colors';
+import { checkboxLayout, checkboxContainer } from './CheckboxWithIcon.styles';
interface CheckboxWithIconProps {
checked: boolean;
@@ -47,36 +48,3 @@ const CheckboxWithIcon = ({
};
export default CheckboxWithIcon;
-
-export const checkboxLayout = (
- theme: Theme,
- align: 'left' | 'center' | 'right',
- size: 'small' | 'large',
- boldStyle: boolean,
-) => css`
- ${size === 'small' ? theme.fonts.label.small.M12 : theme.fonts.label.large.R14};
-
- font-weight: ${boldStyle || size === 'small' ? '500' : '400'};
-
- color: ${boldStyle ? theme.colors.text06 : theme.colors.text04};
-
- margin-top: 0.4rem;
-
- display: flex;
- justify-content: ${align};
-
- cursor: pointer;
-
- label {
- cursor: pointer;
- }
-`;
-
-export const checkboxContainer = (size: 'small' | 'large') => css`
- width: fit-content;
-
- display: flex;
- flex-flow: row nowrap;
- gap: ${size === 'small' ? '0.2rem' : '0.4rem'};
- align-items: center;
-`;
diff --git a/src/app/upload/components/DescriptionSection/DescriptionSection.styles.ts b/src/app/upload/components/DescriptionSection/DescriptionSection.styles.ts
new file mode 100644
index 0000000..93e577c
--- /dev/null
+++ b/src/app/upload/components/DescriptionSection/DescriptionSection.styles.ts
@@ -0,0 +1,129 @@
+import { css, Theme } from '@emotion/react';
+
+export const descriptionFormLayout = css`
+ width: 100%;
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 1.2rem;
+`;
+
+export const fullInput = css`
+ max-width: 93.6rem;
+`;
+
+export const descriptionContentContainer = (theme: Theme, isError: boolean) => css`
+ width: 93.6rem;
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.line01};
+ border-radius: 1.2rem;
+
+ :focus-within {
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.lineTinted};
+ }
+`;
+
+export const descriptionTextarea = (photoGridHeight: number) => (theme: Theme) =>
+ css`
+ ${theme.fonts.label.large.R14};
+
+ border: none;
+ border-top-left-radius: 1.2rem;
+ border-top-right-radius: 1.2rem;
+
+ width: 100%;
+ height: calc(22rem - ${photoGridHeight}rem);
+
+ outline: none;
+
+ padding: 1.4rem 1.6rem;
+
+ resize: none;
+
+ &::placeholder {
+ color: ${theme.colors.text02};
+ }
+ `;
+
+export const uploadImagesContainer = (theme: Theme) => css`
+ height: 5.6rem;
+
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ gap: 1.2rem;
+
+ padding: 1.2rem 1.6rem;
+
+ border-top: 0.1rem solid ${theme.colors.line01};
+
+ p {
+ ${theme.fonts.label.large.R14};
+ color: ${theme.colors.text02};
+ }
+`;
+
+export const addImageContainer = (theme: Theme) => css`
+ ${theme.fonts.label.medium.M13};
+ color: ${theme.colors.text04};
+
+ background-color: ${theme.colors.field04};
+
+ height: 3.2rem;
+
+ display: flex;
+ flex-flow: row nowrap;
+ gap: 0.6rem;
+ align-items: center;
+
+ padding: 0.8rem 1.2rem;
+
+ border-radius: 0.8rem;
+ cursor: pointer;
+
+ p {
+ ${theme.colors.text04}
+ }
+`;
+
+export const photoGrid = css`
+ display: flex;
+ gap: 10px;
+ overflow-x: auto;
+ padding: 0 1.6rem;
+ height: 8rem;
+
+ margin-top: 1rem;
+ margin-bottom: 1.4rem;
+`;
+
+export const photoLayout = css`
+ width: 8rem;
+ height: 8rem;
+
+ border-radius: 4px;
+ background: white;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+`;
+
+export const photoContainer = css`
+ position: relative;
+ width: 8rem;
+ height: 8rem;
+`;
+
+export const deleteButton = css`
+ position: absolute;
+ top: 0.4rem;
+ right: 0.4rem;
+ border: none;
+ border-radius: 50%;
+`;
+
+export const formMessage = (theme: Theme) => css`
+ ${theme.fonts.label.small.M12};
+ color: ${theme.colors.textAlert};
+ margin: 0;
+ padding: 0.8rem 1.6rem;
+`;
diff --git a/src/app/upload/components/DescriptionSection/DescriptionSection.tsx b/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
index d2b1a65..550794d 100644
--- a/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
+++ b/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
@@ -1,14 +1,26 @@
-import { css, Theme } from '@emotion/react';
import Image from 'next/image';
import { useState, ChangeEvent } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import InputForm from '../InputForm/InputForm';
-import { headingIcon, input } from '../UploadContainer/UploadContainer';
import Icon from '@/components/Icon';
import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
import { colors } from '@/styles/colors';
+import { headingIcon, input } from '../UploadContainer/UploadContainer.styles';
+import { formMessage } from '../InputForm/InputForm.styles';
+import {
+ addImageContainer,
+ deleteButton,
+ descriptionContentContainer,
+ descriptionFormLayout,
+ descriptionTextarea,
+ fullInput,
+ photoContainer,
+ photoGrid,
+ photoLayout,
+ uploadImagesContainer,
+} from './DescriptionSection.styles';
type Photo = {
id: string;
@@ -68,7 +80,7 @@ const DescriptionSection = () => {
};
return (
-
+
2어떤 실험인가요?
@@ -164,133 +176,3 @@ const DescriptionSection = () => {
};
export default DescriptionSection;
-
-export const descriptionLayout = css``;
-
-export const descriptionFormLayout = css`
- width: 100%;
- display: flex;
- flex-flow: column nowrap;
- gap: 1.2rem;
-`;
-
-export const fullInput = css`
- max-width: 93.6rem;
-`;
-
-const descriptionContentContainer = (theme: Theme, isError: boolean) => css`
- width: 93.6rem;
- border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.line01};
- border-radius: 1.2rem;
-
- :focus-within {
- border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.lineTinted};
- }
-`;
-
-const descriptionTextarea = (photoGridHeight: number) => (theme: Theme) =>
- css`
- ${theme.fonts.label.large.R14};
-
- border: none;
- border-top-left-radius: 1.2rem;
- border-top-right-radius: 1.2rem;
-
- width: 100%;
- height: calc(22rem - ${photoGridHeight}rem);
-
- outline: none;
-
- padding: 1.4rem 1.6rem;
-
- resize: none;
-
- &::placeholder {
- color: ${theme.colors.text02};
- }
- `;
-
-const uploadImagesContainer = (theme: Theme) => css`
- height: 5.6rem;
-
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
- gap: 1.2rem;
-
- padding: 1.2rem 1.6rem;
-
- border-top: 0.1rem solid ${theme.colors.line01};
-
- p {
- ${theme.fonts.label.large.R14};
- color: ${theme.colors.text02};
- }
-`;
-
-const addImageContainer = (theme: Theme) => css`
- ${theme.fonts.label.medium.M13};
- color: ${theme.colors.text04};
-
- background-color: ${theme.colors.field04};
-
- height: 3.2rem;
-
- display: flex;
- flex-flow: row nowrap;
- gap: 0.6rem;
- align-items: center;
-
- padding: 0.8rem 1.2rem;
-
- border-radius: 0.8rem;
- cursor: pointer;
-
- p {
- ${theme.colors.text04}
- }
-`;
-
-const photoGrid = css`
- display: flex;
- gap: 10px;
- overflow-x: auto;
- padding: 0 1.6rem;
- height: 8rem;
-
- margin-top: 1rem;
- margin-bottom: 1.4rem;
-`;
-
-const photoLayout = css`
- width: 8rem;
- height: 8rem;
-
- border-radius: 4px;
- background: white;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
-`;
-
-const photoContainer = css`
- position: relative;
- width: 8rem;
- height: 8rem;
-`;
-
-const deleteButton = css`
- position: absolute;
- top: 0.4rem;
- right: 0.4rem;
- border: none;
- border-radius: 50%;
-`;
-
-const formMessage = (theme: Theme) => css`
- ${theme.fonts.label.small.M12};
- color: ${theme.colors.textAlert};
- margin: 0;
- padding: 0.8rem 1.6rem;
-`;
diff --git a/src/app/upload/components/InputForm/InputForm.styles.ts b/src/app/upload/components/InputForm/InputForm.styles.ts
new file mode 100644
index 0000000..5677705
--- /dev/null
+++ b/src/app/upload/components/InputForm/InputForm.styles.ts
@@ -0,0 +1,52 @@
+import { css, Theme } from '@emotion/react';
+
+export const textInputContainer = (size: 'half' | 'full') => css`
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ position: relative;
+
+ width: 100%;
+ max-width: ${size === 'half' ? '45.2rem' : '93.6rem'};
+`;
+
+export const textCounter = (theme: Theme) => css`
+ ${theme.fonts.label.small.M12};
+ color: ${theme.colors.text02};
+
+ text-align: right;
+`;
+
+export const textSubMessageLayout = (showTextCounter: boolean) => css`
+ display: flex;
+ flex-flow: ${showTextCounter ? 'row-reverse nowrap' : 'row nowrap'};
+ justify-content: space-between;
+ align-items: center;
+`;
+
+export const formMessage = (theme: Theme) => css`
+ ${theme.fonts.label.small.M12};
+ color: ${theme.colors.textAlert};
+ margin: 0;
+`;
+
+export const textInput = (theme: Theme, status: string) => css`
+ ${theme.fonts.label.large.R14};
+
+ width: 100%;
+ height: 4.8rem;
+ padding: 0.8rem 1.2rem;
+ border: 0.1rem solid ${status === 'error' ? theme.colors.textAlert : theme.colors.line01};
+ border-radius: 1.2rem;
+ color: ${theme.colors.text06};
+
+ &:focus {
+ border-color: ${status === 'error' ? theme.colors.textAlert : theme.colors.lineTinted};
+ outline: none;
+ }
+
+ &::placeholder {
+ color: ${theme.colors.text02};
+ ${theme.fonts.label.large.R14};
+ }
+`;
diff --git a/src/app/upload/components/InputForm/InputForm.tsx b/src/app/upload/components/InputForm/InputForm.tsx
index 4dc5d17..b93973e 100644
--- a/src/app/upload/components/InputForm/InputForm.tsx
+++ b/src/app/upload/components/InputForm/InputForm.tsx
@@ -1,5 +1,11 @@
-import { css, Theme } from '@emotion/react';
import React, { ChangeEvent, forwardRef, useState } from 'react';
+import {
+ textInputContainer,
+ textInput,
+ textSubMessageLayout,
+ textCounter,
+ formMessage,
+} from './InputForm.styles';
interface InputFormProps {
id: string;
@@ -76,54 +82,3 @@ const InputForm = forwardRef
(
InputForm.displayName = 'InputForm';
export default InputForm;
-
-const textInputContainer = (size: 'half' | 'full') => css`
- display: flex;
- flex-direction: column;
- gap: 0.4rem;
- position: relative;
-
- width: 100%;
- max-width: ${size === 'half' ? '45.2rem' : '93.6rem'};
-`;
-
-const textCounter = (theme: Theme) => css`
- ${theme.fonts.label.small.M12};
- color: ${theme.colors.text02};
-
- text-align: right;
-`;
-
-const textSubMessageLayout = (showTextCounter: boolean) => css`
- display: flex;
- flex-flow: ${showTextCounter ? 'row-reverse nowrap' : 'row nowrap'};
- justify-content: space-between;
- align-items: center;
-`;
-
-const formMessage = (theme: Theme) => css`
- ${theme.fonts.label.small.M12};
- color: ${theme.colors.textAlert};
- margin: 0;
-`;
-
-const textInput = (theme: Theme, status: string) => css`
- ${theme.fonts.label.large.R14};
-
- width: 100%;
- height: 4.8rem;
- padding: 0.8rem 1.2rem;
- border: 0.1rem solid ${status === 'error' ? theme.colors.textAlert : theme.colors.line01};
- border-radius: 1.2rem;
- color: ${theme.colors.text06};
-
- &:focus {
- border-color: ${status === 'error' ? theme.colors.textAlert : theme.colors.lineTinted};
- outline: none;
- }
-
- &::placeholder {
- color: ${theme.colors.text02};
- ${theme.fonts.label.large.R14};
- }
-`;
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.styles.ts b/src/app/upload/components/OutlineSection/OutlineSection.styles.ts
new file mode 100644
index 0000000..23d7c66
--- /dev/null
+++ b/src/app/upload/components/OutlineSection/OutlineSection.styles.ts
@@ -0,0 +1,67 @@
+import { css, Theme } from '@emotion/react';
+
+export const outlineFormLayout = css`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 10.2rem 10.2rem auto;
+
+ grid-column-gap: 3.2rem;
+ grid-row-gap: 2.8rem;
+
+ margin: 0 auto;
+`;
+
+export const radioGroup = css`
+ display: flex;
+ flex-flow: row nowrap;
+ gap: 0.8rem;
+`;
+
+export const customRadioGroup = css`
+ display: flex;
+ gap: 1rem;
+`;
+
+export const customRadioButton = (theme: Theme) => css`
+ ${theme.fonts.label.large.M14};
+
+ width: 14.533rem;
+ height: 4.8rem;
+
+ padding: 1rem 2rem;
+
+ border: 0.1rem solid ${theme.colors.line01};
+ border-radius: 1.2rem;
+
+ background-color: ${theme.colors.field01};
+
+ cursor: pointer;
+
+ transition: all 0.2s ease;
+
+ &:hover {
+ background-color: ${theme.colors.field02};
+ }
+`;
+
+export const activeRadioButton = (theme: Theme) => css`
+ border: 0.1rem solid ${theme.colors.lineTinted};
+
+ background-color: ${theme.colors.primaryTinted};
+ color: ${theme.colors.textPrimary};
+
+ &:hover {
+ background-color: ${theme.colors.primaryTinted};
+ }
+`;
+
+export const inputContainer = css`
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 0.8rem;
+`;
+
+export const disabledInput = (theme: Theme) => css`
+ background-color: ${theme.colors.field02};
+ color: ${theme.colors.text02};
+`;
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index fa204aa..48925e7 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -1,4 +1,3 @@
-import { css, Theme } from '@emotion/react';
import { useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
@@ -8,14 +7,21 @@ import InputForm from '../InputForm/InputForm';
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
import RegionPopover from '../RegionPopover/RegionPopover';
import SelectForm from '../SelectForm/SelectForm';
-import { headingIcon, input, label } from '../UploadContainer/UploadContainer';
import DatePickerForm from '@/app/upload/components/DatePickerForm/DatePickerForm';
import { colors } from '@/styles/colors';
import { MatchType } from '@/types/uploadExperimentPost';
+import {
+ headingIcon,
+ label,
+ input,
+ outlineFormLayout,
+} from '../UploadContainer/UploadContainer.styles';
+import { inputContainer } from '@/app/join/components/JoinInput/JoinInput.styles';
+import { disabledInput } from './OutlineSection.styles';
const OutlineSection = () => {
- const { control, setValue, formState } = useFormContext();
+ const { control, setValue } = useFormContext();
// 실험 일시 및 소요시간 본문 참고 여부
const [experimentDateChecked, setExperimentDateChecked] = useState(false);
@@ -327,69 +333,3 @@ const OutlineSection = () => {
};
export default OutlineSection;
-
-export const outlineFormLayout = css`
- display: grid;
- grid-template-columns: 1fr 1fr;
- grid-template-rows: 10.2rem 10.2rem auto;
-
- grid-column-gap: 3.2rem;
- grid-row-gap: 2.8rem;
-
- margin: 0 auto;
-`;
-
-export const radioGroup = css`
- display: flex;
- flex-flow: row nowrap;
- gap: 0.8rem;
-`;
-
-export const customRadioGroup = css`
- display: flex;
- gap: 1rem;
-`;
-
-export const customRadioButton = (theme: Theme) => css`
- ${theme.fonts.label.large.M14};
-
- width: 14.533rem;
- height: 4.8rem;
-
- padding: 1rem 2rem;
-
- border: 0.1rem solid ${theme.colors.line01};
- border-radius: 1.2rem;
-
- background-color: ${theme.colors.field01};
-
- cursor: pointer;
-
- transition: all 0.2s ease;
-
- &:hover {
- background-color: ${theme.colors.field02};
- }
-`;
-
-export const activeRadioButton = (theme: Theme) => css`
- border: 0.1rem solid ${theme.colors.lineTinted};
-
- background-color: ${theme.colors.primaryTinted};
- color: ${theme.colors.textPrimary};
-
- &:hover {
- background-color: ${theme.colors.primaryTinted};
- }
-`;
-
-const inputContainer = css`
- display: flex;
- flex-flow: column nowrap;
- gap: 0.8rem;
-`;
-
-export const disabledInput = (theme: Theme) => css`
- background-color: ${theme.colors.field02};
- color: ${theme.colors.text02};
-`;
diff --git a/src/app/upload/components/RegionPopover/RegionPopover.tsx b/src/app/upload/components/RegionPopover/RegionPopover.tsx
index f2099f5..0d272bb 100644
--- a/src/app/upload/components/RegionPopover/RegionPopover.tsx
+++ b/src/app/upload/components/RegionPopover/RegionPopover.tsx
@@ -14,10 +14,10 @@ import {
placeholderText,
regionPopoverContainer,
} from './RegionPopover.styles';
-import { input } from '../UploadContainer/UploadContainer';
import Icon from '@/components/Icon';
import { UPLOAD_REGION } from '@/constants/uploadRegion';
+import { input } from '../UploadContainer/UploadContainer.styles';
interface RegionPopoverProps {
regionPopoverProps: {
diff --git a/src/app/upload/components/TextInput/TextInput.tsx b/src/app/upload/components/TextInput/TextInput.tsx
deleted file mode 100644
index 3c16d04..0000000
--- a/src/app/upload/components/TextInput/TextInput.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import { css, Theme } from '@emotion/react';
-import { forwardRef, useState } from 'react';
-
-interface TextInputProps {
- id: string;
- placeholder: string;
- maxLength?: number;
- size?: 'half' | 'full';
- field?: {
- name: string;
- value: string;
- onChange: (event: React.ChangeEvent) => void;
- onBlur: VoidFunction;
- };
- fieldState?: {
- error?: {
- message?: string;
- };
- };
-}
-
-export const TextInput = forwardRef(
- ({ id, placeholder, maxLength, size = 'half', field, fieldState }, ref) => {
- const [textLength, setTextLength] = useState(0);
-
- const handleChange = (e: React.ChangeEvent) => {
- if (field?.onChange) {
- field.onChange(e);
- }
-
- setTextLength(e.target.value.length);
- };
-
- return (
-
-
textInput(theme, fieldState?.error ? 'error' : '')}
- />
-
- {maxLength && (
-
- {textLength}/{maxLength}
-
- )}
- {fieldState?.error &&
{fieldState.error.message}
}
-
-
- );
- },
-);
-
-TextInput.displayName = 'TextInput';
-
-const textInputContainer = (size: 'half' | 'full') => css`
- display: flex;
- flex-direction: column;
- gap: 0.4rem;
- position: relative;
-
- width: 100%;
- max-width: ${size === 'half' ? '45.2rem' : '93.6rem'};
-`;
-
-const textInput = (theme: Theme, status: string) => css`
- ${theme.fonts.label.large.R14};
-
- width: 100%;
- height: 4.8rem;
- padding: 0.8rem 1.2rem;
- border: 0.1rem solid ${status === 'error' ? theme.colors.textAlert : theme.colors.line01};
- border-radius: 1.2rem;
- color: ${theme.colors.text06};
-
- &:focus {
- border-color: ${status === 'error' ? theme.colors.textAlert : theme.colors.lineTinted};
- outline: none;
- }
-
- &::placeholder {
- color: ${theme.colors.text02};
- ${theme.fonts.label.large.R14};
- }
-`;
-
-const textCounter = (theme: Theme) => css`
- ${theme.fonts.label.small.M12};
- color: ${theme.colors.text02};
-
- text-align: right;
-`;
-
-const formMessage = (theme: Theme) => css`
- ${theme.fonts.label.small.M12};
- color: ${theme.colors.textAlert};
- margin: 0;
-`;
-
-const textSubMessageLayout = css`
- display: flex;
- flex-flow: row-reverse nowrap;
- justify-content: space-between;
- align-items: center;
-`;
diff --git a/src/app/upload/components/UploadContainer/UploadContainer.styles.ts b/src/app/upload/components/UploadContainer/UploadContainer.styles.ts
new file mode 100644
index 0000000..d1ad387
--- /dev/null
+++ b/src/app/upload/components/UploadContainer/UploadContainer.styles.ts
@@ -0,0 +1,141 @@
+import { css, Theme } from '@emotion/react';
+
+export const uploadLayout = (theme: Theme) => css`
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 4rem;
+
+ color: ${theme.colors.text06};
+
+ h3 {
+ ${theme.fonts.title.small.SB18};
+
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ gap: 0.8rem;
+
+ margin-bottom: 2rem;
+ }
+`;
+
+export const headerContainer = (theme: Theme) => css`
+ h2 {
+ ${theme.fonts.title.large.SB24};
+ }
+
+ p {
+ ${theme.fonts.label.large.R14};
+ color: ${theme.colors.text03};
+ }
+`;
+
+export const uploadContentLayout = (theme: Theme) => css`
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 4.8rem;
+
+ > div {
+ background-color: ${theme.colors.field01};
+ border-radius: 1.2rem;
+
+ padding: 3.2rem 2.8rem;
+ }
+`;
+
+export const headingIcon = (theme: Theme) => css`
+ ${theme.fonts.label.small.SB12};
+
+ width: 1.8rem;
+ height: 1.8rem;
+
+ border-radius: 50%;
+ text-align: center;
+ padding: 0.2rem;
+
+ background-color: ${theme.colors.primaryMint};
+ color: ${theme.colors.text01};
+`;
+
+export const buttonContainer = (theme: Theme) => css`
+ ${theme.fonts.body.normal.B16};
+
+ width: 100%;
+ height: 4rem;
+
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ justify-content: center;
+ gap: 1.2rem;
+`;
+
+export const activeButton = (theme: Theme) => css`
+ color: ${theme.colors.text06};
+ background-color: ${theme.colors.field04};
+ border-radius: 1.2rem;
+ padding: 0.8rem 1.6rem;
+`;
+
+export const uploadButton = (theme: Theme) => css`
+ color: ${theme.colors.text01};
+ background-color: ${theme.colors.field09};
+ border-radius: 1.2rem;
+ padding: 0.8rem 1.6rem;
+`;
+
+export const outlineFormLayout = css`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 10.2rem 10.2rem 21.4rem;
+
+ grid-column-gap: 3.2rem;
+ grid-row-gap: 2.8rem;
+
+ margin: 0 auto;
+`;
+
+export const input = (theme: Theme, isError?: boolean) => css`
+ ${theme.fonts.label.large.R14};
+
+ width: 100%;
+ max-width: 45.2rem;
+ height: 4.8rem;
+
+ padding: 10px;
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.line01};
+ border-radius: 1.2rem;
+
+ outline: none;
+
+ &::placeholder {
+ color: ${theme.colors.text02};
+ }
+
+ &:focus {
+ outline: 0.1rem solid ${theme.colors.lineTinted};
+ outline-offset: 0;
+
+ border: none;
+ }
+`;
+
+export const label = (theme: Theme) => css`
+ ${theme.fonts.label.large.M14};
+ color: ${theme.colors.text05};
+
+ margin-bottom: 0.8rem;
+ display: block;
+`;
+
+export const ReferToDetailsContainer = (theme: Theme) => css`
+ ${theme.fonts.label.small.M12};
+ color: ${theme.colors.text04};
+
+ margin-top: 0.4rem;
+ display: flex;
+ align-items: center;
+ gap: 0.2rem;
+
+ justify-content: right;
+`;
diff --git a/src/app/upload/components/UploadContainer/UploadContainer.tsx b/src/app/upload/components/UploadContainer/UploadContainer.tsx
index ca1fa57..a46b5fa 100644
--- a/src/app/upload/components/UploadContainer/UploadContainer.tsx
+++ b/src/app/upload/components/UploadContainer/UploadContainer.tsx
@@ -1,7 +1,5 @@
'use client';
-import { Theme } from '@emotion/react';
-import { css } from '@emotion/react';
import Link from 'next/link';
import React, { useState } from 'react';
import { FormProvider } from 'react-hook-form';
@@ -10,6 +8,14 @@ import useUploadExperimentPost from '../../hooks/useUploadExperimentPost';
import ApplyMethodSection from '../ApplyMethodSection/ApplyMethodSection';
import DescriptionSection from '../DescriptionSection/DescriptionSection';
import OutlineSection from '../OutlineSection/OutlineSection';
+import {
+ activeButton,
+ buttonContainer,
+ headerContainer,
+ uploadButton,
+ uploadContentLayout,
+ uploadLayout,
+} from './UploadContainer.styles';
const UploadContainer = () => {
const [addLink, setAddLink] = useState(false);
@@ -56,143 +62,3 @@ const UploadContainer = () => {
};
export default UploadContainer;
-
-export const uploadLayout = (theme: Theme) => css`
- display: flex;
- flex-flow: column nowrap;
- gap: 4rem;
-
- color: ${theme.colors.text06};
-
- h3 {
- ${theme.fonts.title.small.SB18};
-
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
- gap: 0.8rem;
-
- margin-bottom: 2rem;
- }
-`;
-
-export const headerContainer = (theme: Theme) => css`
- h2 {
- ${theme.fonts.title.large.SB24};
- }
-
- p {
- ${theme.fonts.label.large.R14};
- color: ${theme.colors.text03};
- }
-`;
-
-export const uploadContentLayout = (theme: Theme) => css`
- display: flex;
- flex-flow: column nowrap;
- gap: 4.8rem;
-
- > div {
- background-color: ${theme.colors.field01};
- border-radius: 1.2rem;
-
- padding: 3.2rem 2.8rem;
- }
-`;
-
-export const headingIcon = (theme: Theme) => css`
- ${theme.fonts.label.small.SB12};
-
- width: 1.8rem;
- height: 1.8rem;
-
- border-radius: 50%;
- text-align: center;
- padding: 0.2rem;
-
- background-color: ${theme.colors.primaryMint};
- color: ${theme.colors.text01};
-`;
-
-export const buttonContainer = (theme: Theme) => css`
- ${theme.fonts.body.normal.B16};
-
- width: 100%;
- height: 4rem;
-
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
- justify-content: center;
- gap: 1.2rem;
-`;
-
-const activeButton = (theme: Theme) => css`
- color: ${theme.colors.text06};
- background-color: ${theme.colors.field04};
- border-radius: 1.2rem;
- padding: 0.8rem 1.6rem;
-`;
-
-const uploadButton = (theme: Theme) => css`
- color: ${theme.colors.text01};
- background-color: ${theme.colors.field09};
- border-radius: 1.2rem;
- padding: 0.8rem 1.6rem;
-`;
-
-export const outlineFormLayout = css`
- display: grid;
- grid-template-columns: 1fr 1fr;
- grid-template-rows: 10.2rem 10.2rem 21.4rem;
-
- grid-column-gap: 3.2rem;
- grid-row-gap: 2.8rem;
-
- margin: 0 auto;
-`;
-
-export const input = (theme: Theme, isError?: boolean) => css`
- ${theme.fonts.label.large.R14};
-
- width: 100%;
- max-width: 45.2rem;
- height: 4.8rem;
-
- padding: 10px;
- border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.line01};
- border-radius: 1.2rem;
-
- outline: none;
-
- &::placeholder {
- color: ${theme.colors.text02};
- }
-
- &:focus {
- outline: 0.1rem solid ${theme.colors.lineTinted};
- outline-offset: 0;
-
- border: none;
- }
-`;
-
-export const label = (theme: Theme) => css`
- ${theme.fonts.label.large.M14};
- color: ${theme.colors.text05};
-
- margin-bottom: 0.8rem;
- display: block;
-`;
-
-export const ReferToDetailsContainer = (theme: Theme) => css`
- ${theme.fonts.label.small.M12};
- color: ${theme.colors.text04};
-
- margin-top: 0.4rem;
- display: flex;
- align-items: center;
- gap: 0.2rem;
-
- justify-content: right;
-`;
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index 01817f5..ce8a36e 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -4,10 +4,10 @@ import { useForm } from 'react-hook-form';
import { convertLabelToValue } from '../upload.utils';
-import useUploadExperimentPostAPI from '@/apis/useUploadExperimentPostAPI';
import UploadExperimentPostSchema, {
UploadExperimentPostSchemaType,
} from '@/schema/upload/uploadExperimentPostSchema';
+import useUploadExperimentPostAPI from '@/apis/hooks/useUploadExperimentPostAPI';
interface useUploadExperimentPostProps {
addLink: boolean;
From 9f2ec011398c92501f0b143c3f197dd0af977b91 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Sat, 25 Jan 2025 02:33:39 +0900
Subject: [PATCH 28/32] =?UTF-8?q?[YS-172]=20refactor:=20api=20import=20?=
=?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/apis/hooks/useUploadExperimentPostAPI.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/apis/hooks/useUploadExperimentPostAPI.ts b/src/apis/hooks/useUploadExperimentPostAPI.ts
index 6affeec..d71cdf9 100644
--- a/src/apis/hooks/useUploadExperimentPostAPI.ts
+++ b/src/apis/hooks/useUploadExperimentPostAPI.ts
@@ -1,10 +1,9 @@
import { useMutation } from '@tanstack/react-query';
-import { API } from './config';
-
import { GenderType } from '@/app/upload/components/ApplyMethodSection/ApplyMethodSection';
import { MatchType } from '@/types/uploadExperimentPost';
import { API_URL } from '@/constants/url';
+import { API } from '../config';
interface UseUploadExperimentPostAPIParams {
startDate?: string | null;
From c20498fb54674021799caef3821e8d2c35a260f5 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Sat, 25 Jan 2025 03:19:34 +0900
Subject: [PATCH 29/32] =?UTF-8?q?[YS-172]=20refactor:=20blur=EC=9D=BC=20?=
=?UTF-8?q?=EB=95=8C=20border=20=EC=83=89=EC=83=81=20=EC=A0=81=EC=9A=A9=20?=
=?UTF-8?q?=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DescriptionSection/DescriptionSection.tsx | 4 ++--
.../OutlineSection/OutlineSection.styles.ts | 2 +-
.../OutlineSection/OutlineSection.tsx | 17 ++++++++---------
.../components/RegionPopover/RegionPopover.tsx | 4 ++--
.../components/SelectForm/SelectForm.styles.ts | 11 +++++++----
.../upload/components/SelectForm/SelectForm.tsx | 10 +++++++---
.../UploadContainer/UploadContainer.styles.ts | 2 +-
.../upload/hooks/useUploadExperimentPost.tsx | 6 +-----
src/schema/upload/uploadExperimentPostSchema.ts | 7 +------
9 files changed, 30 insertions(+), 33 deletions(-)
diff --git a/src/app/upload/components/DescriptionSection/DescriptionSection.tsx b/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
index 550794d..b0ee203 100644
--- a/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
+++ b/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
@@ -7,7 +7,7 @@ import InputForm from '../InputForm/InputForm';
import Icon from '@/components/Icon';
import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
import { colors } from '@/styles/colors';
-import { headingIcon, input } from '../UploadContainer/UploadContainer.styles';
+import { headingIcon, uploadInput } from '../UploadContainer/UploadContainer.styles';
import { formMessage } from '../InputForm/InputForm.styles';
import {
addImageContainer,
@@ -93,7 +93,7 @@ const DescriptionSection = () => {
render={({ field, fieldState }) => (
css`
}
`;
-export const inputContainer = css`
+export const uploadInputContainer = css`
display: flex;
flex-flow: column nowrap;
gap: 0.8rem;
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index 48925e7..72c2923 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -14,14 +14,13 @@ import { MatchType } from '@/types/uploadExperimentPost';
import {
headingIcon,
label,
- input,
+ uploadInput,
outlineFormLayout,
} from '../UploadContainer/UploadContainer.styles';
-import { inputContainer } from '@/app/join/components/JoinInput/JoinInput.styles';
-import { disabledInput } from './OutlineSection.styles';
+import { disabledInput, uploadInputContainer } from './OutlineSection.styles';
const OutlineSection = () => {
- const { control, setValue } = useFormContext();
+ const { control, setValue, formState } = useFormContext();
// 실험 일시 및 소요시간 본문 참고 여부
const [experimentDateChecked, setExperimentDateChecked] = useState(false);
@@ -104,7 +103,7 @@ const OutlineSection = () => {
{
실험 장소 *
{selectedMatchType === MatchType.ONLINE ? (
- 비대면
+ 비대면
) : (
-
+
{
{
소요 시간 *
-
+
{/* 실험 횟수 */}
{
}
}}
>
- [input, regionField(theme)]}>
+
[uploadInput, regionField(theme)]}>
placeholderText(theme, !!(selectedRegion && selectedSubRegion))}>
{selectedRegion && selectedSubRegion
? `${regionData?.label} ${selectedSubRegion}`
diff --git a/src/app/upload/components/SelectForm/SelectForm.styles.ts b/src/app/upload/components/SelectForm/SelectForm.styles.ts
index c2b8a7f..c154628 100644
--- a/src/app/upload/components/SelectForm/SelectForm.styles.ts
+++ b/src/app/upload/components/SelectForm/SelectForm.styles.ts
@@ -6,14 +6,16 @@ export const selectInputContainer = css`
gap: 0.4rem;
`;
-export const selectTrigger = (theme: Theme, disabled: boolean, status: string) => css`
+export const selectTrigger = (theme: Theme, disabled: boolean, isError: boolean) => css`
${theme.fonts.label.large.R14};
width: 100%;
height: 4.8rem;
padding: 0.8rem 1.2rem;
- border: 0.1rem solid ${status === 'error' ? theme.colors.textAlert : theme.colors.line01};
+
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.line01};
border-radius: 1.2rem;
+
color: ${disabled ? theme.colors.text02 : theme.colors.text06};
background-color: ${disabled ? theme.colors.field02 : 'transparent'};
@@ -25,8 +27,9 @@ export const selectTrigger = (theme: Theme, disabled: boolean, status: string) =
color: ${theme.colors.text02};
}
- &:focus {
- border-color: ${status === 'error' ? theme.colors.textAlert : theme.colors.lineTinted};
+ &[data-state='open'] {
+ border: 0.1rem solid ${theme.colors.primaryMint};
+ border: 0.1rem solid ${isError ? theme.colors.textAlert : theme.colors.primaryMint};
outline: none;
}
`;
diff --git a/src/app/upload/components/SelectForm/SelectForm.tsx b/src/app/upload/components/SelectForm/SelectForm.tsx
index 74a8b9c..3dbc7b2 100644
--- a/src/app/upload/components/SelectForm/SelectForm.tsx
+++ b/src/app/upload/components/SelectForm/SelectForm.tsx
@@ -44,13 +44,17 @@ const SelectForm = forwardRef(
{
+ setIsOpen(open);
+ if (!open) {
+ field.onBlur();
+ }
+ }}
disabled={disabled}
>
selectTrigger(theme, disabled, fieldState?.error ? 'error' : '')}
- onBlur={field?.onBlur}
+ css={(theme) => selectTrigger(theme, disabled, !!fieldState?.error)}
>
diff --git a/src/app/upload/components/UploadContainer/UploadContainer.styles.ts b/src/app/upload/components/UploadContainer/UploadContainer.styles.ts
index d1ad387..13bfa56 100644
--- a/src/app/upload/components/UploadContainer/UploadContainer.styles.ts
+++ b/src/app/upload/components/UploadContainer/UploadContainer.styles.ts
@@ -95,7 +95,7 @@ export const outlineFormLayout = css`
margin: 0 auto;
`;
-export const input = (theme: Theme, isError?: boolean) => css`
+export const uploadInput = (theme: Theme, isError?: boolean) => css`
${theme.fonts.label.large.R14};
width: 100%;
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index ce8a36e..df9987c 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -20,11 +20,7 @@ const useUploadExperimentPost = ({ addLink, addContact }: useUploadExperimentPos
const form = useForm({
mode: 'onBlur',
reValidateMode: 'onChange',
- resolver: async (data, context, options) => {
- const matchType = data.matchType;
- const schema = UploadExperimentPostSchema({ matchType, addLink, addContact });
- return zodResolver(schema)(data, context, options);
- },
+ resolver: zodResolver(UploadExperimentPostSchema({ addLink, addContact })),
defaultValues: {
leadResearcher: '',
startDate: undefined,
diff --git a/src/schema/upload/uploadExperimentPostSchema.ts b/src/schema/upload/uploadExperimentPostSchema.ts
index f655be0..5467861 100644
--- a/src/schema/upload/uploadExperimentPostSchema.ts
+++ b/src/schema/upload/uploadExperimentPostSchema.ts
@@ -6,15 +6,10 @@ import { MatchType } from '@/types/uploadExperimentPost';
export type UploadExperimentPostSchemaType = z.infer>;
interface UploadExperimentPostSchemaProps {
- matchType: MatchType;
addLink: boolean;
addContact: boolean;
}
-const UploadExperimentPostSchema = ({
- matchType,
- addLink,
- addContact,
-}: UploadExperimentPostSchemaProps) => {
+const UploadExperimentPostSchema = ({ addLink, addContact }: UploadExperimentPostSchemaProps) => {
return z.object({
// 실험 시작 날짜
startDate: z.union([z.string(), z.null()]),
From 0f60274c6107d7a13b0d8837816ac90448d949b1 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Sat, 25 Jan 2025 03:28:32 +0900
Subject: [PATCH 30/32] =?UTF-8?q?[YS-172]=20design:=20=EA=B3=B5=EA=B3=A0?=
=?UTF-8?q?=20=EB=93=B1=EB=A1=9D=20=ED=95=84=EC=88=98=EC=9E=85=EB=A0=A5?=
=?UTF-8?q?=EA=B0=92=20=ED=91=9C=EC=8B=9C=20=EB=94=94=EC=9E=90=EC=9D=B8=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ApplyMethodSection/ApplyMethodSection.tsx | 14 +++++------
.../DescriptionSection/DescriptionSection.tsx | 3 ++-
.../OutlineSection/OutlineSection.tsx | 23 ++++++++-----------
3 files changed, 17 insertions(+), 23 deletions(-)
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index 8db1214..2592d0b 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -51,7 +51,8 @@ const ApplyMethodSection = ({
{/* 실험 참여 방법 */}
- 3실험에 참여하려면 어떻게 하면 되나요?
+ 3실험에 참여하려면 어떻게 하면 되나요?{' '}
+ *
@@ -136,15 +137,14 @@ const ApplyMethodSection = ({
{/* 모집 조건 */}
- 4어떤 사람들을 모집하나요?
+ 4어떤 사람들을 모집하나요?{' '}
+ *
{/* 나이 */}
-
- 나이 *
-
+
나이
ageInputContainer(theme, ageError)}>
만
-
- 성별 *
-
+ 성별
{
return (
- 2어떤 실험인가요?
+ 2어떤 실험인가요?{' '}
+ *
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index 72c2923..806ea3f 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -20,7 +20,7 @@ import {
import { disabledInput, uploadInputContainer } from './OutlineSection.styles';
const OutlineSection = () => {
- const { control, setValue, formState } = useFormContext();
+ const { control, setValue } = useFormContext();
// 실험 일시 및 소요시간 본문 참고 여부
const [experimentDateChecked, setExperimentDateChecked] = useState(false);
@@ -87,14 +87,15 @@ const OutlineSection = () => {
return (
- 1실험의 개요를 알려주세요
+ 1실험의 개요를 알려주세요{' '}
+ *
{/* 연구 책임자 */}
- 연구 책임자 *
+ 연구 책임자
{
{/* 실험 일시 */}
-
- 실험 일시 *
-
+
실험 일시
{/* 날짜 선택 */}
{
{/* 진행 방식 */}
-
- 진행 방식 *
-
+
진행 방식
{
{/* 참여 보상 */}
- 참여 보상 *
+ 참여 보상
{
{/* 실험 장소 */}
- 실험 장소 *
+ 실험 장소
{selectedMatchType === MatchType.ONLINE ? (
비대면
@@ -275,9 +272,7 @@ const OutlineSection = () => {
{/* 소요 시간 */}
-
- 소요 시간 *
-
+
소요 시간
{/* 실험 횟수 */}
From 3ffd65c815df22722372db4bfbd29d98a04d5a2b Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Sat, 25 Jan 2025 03:39:29 +0900
Subject: [PATCH 31/32] =?UTF-8?q?[YS-172]=20refactor:=20lint=20=ED=95=B4?=
=?UTF-8?q?=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ApplyMethodSection/ApplyMethodSection.tsx | 17 ++++++++---------
.../CheckboxWithIcon/CheckboxWithIcon.tsx | 5 +++--
.../DescriptionSection/DescriptionSection.tsx | 14 +++++++-------
.../upload/components/InputForm/InputForm.tsx | 1 +
.../OutlineSection/OutlineSection.tsx | 10 +++++-----
.../components/RegionPopover/RegionPopover.tsx | 4 ++--
.../UploadContainer/UploadContainer.tsx | 8 ++++----
7 files changed, 30 insertions(+), 29 deletions(-)
diff --git a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
index 2592d0b..2b5694e 100644
--- a/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
+++ b/src/app/upload/components/ApplyMethodSection/ApplyMethodSection.tsx
@@ -1,15 +1,6 @@
-import { css, Theme } from '@emotion/react';
import { Dispatch, SetStateAction } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
-import AgeForm from '../AgeForm/AgeForm';
-import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
-import InputForm from '../InputForm/InputForm';
-import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
-
-import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
-import { colors } from '@/styles/colors';
-import { headingIcon, label } from '../UploadContainer/UploadContainer.styles';
import {
addContactInfoContainer,
ageInputContainer,
@@ -20,6 +11,14 @@ import {
targetGroupContainer,
textStyle,
} from './ApplyMethodSection.styles';
+import AgeForm from '../AgeForm/AgeForm';
+import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
+import InputForm from '../InputForm/InputForm';
+import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
+import { headingIcon, label } from '../UploadContainer/UploadContainer.styles';
+
+import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
+import { colors } from '@/styles/colors';
export enum GenderType {
MALE = 'MALE',
diff --git a/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx b/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx
index 76fe820..cd82fb1 100644
--- a/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx
+++ b/src/app/upload/components/CheckboxWithIcon/CheckboxWithIcon.tsx
@@ -1,8 +1,9 @@
-import { css, Theme } from '@emotion/react';
+import { Theme } from '@emotion/react';
+
+import { checkboxLayout, checkboxContainer } from './CheckboxWithIcon.styles';
import Icon from '@/components/Icon';
import { colors } from '@/styles/colors';
-import { checkboxLayout, checkboxContainer } from './CheckboxWithIcon.styles';
interface CheckboxWithIconProps {
checked: boolean;
diff --git a/src/app/upload/components/DescriptionSection/DescriptionSection.tsx b/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
index 3b9a9c5..b1347d6 100644
--- a/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
+++ b/src/app/upload/components/DescriptionSection/DescriptionSection.tsx
@@ -2,13 +2,6 @@ import Image from 'next/image';
import { useState, ChangeEvent } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
-import InputForm from '../InputForm/InputForm';
-
-import Icon from '@/components/Icon';
-import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
-import { colors } from '@/styles/colors';
-import { headingIcon, uploadInput } from '../UploadContainer/UploadContainer.styles';
-import { formMessage } from '../InputForm/InputForm.styles';
import {
addImageContainer,
deleteButton,
@@ -21,6 +14,13 @@ import {
photoLayout,
uploadImagesContainer,
} from './DescriptionSection.styles';
+import InputForm from '../InputForm/InputForm';
+import { formMessage } from '../InputForm/InputForm.styles';
+import { headingIcon, uploadInput } from '../UploadContainer/UploadContainer.styles';
+
+import Icon from '@/components/Icon';
+import { UploadExperimentPostSchemaType } from '@/schema/upload/uploadExperimentPostSchema';
+import { colors } from '@/styles/colors';
type Photo = {
id: string;
diff --git a/src/app/upload/components/InputForm/InputForm.tsx b/src/app/upload/components/InputForm/InputForm.tsx
index b93973e..58011e4 100644
--- a/src/app/upload/components/InputForm/InputForm.tsx
+++ b/src/app/upload/components/InputForm/InputForm.tsx
@@ -1,4 +1,5 @@
import React, { ChangeEvent, forwardRef, useState } from 'react';
+
import {
textInputContainer,
textInput,
diff --git a/src/app/upload/components/OutlineSection/OutlineSection.tsx b/src/app/upload/components/OutlineSection/OutlineSection.tsx
index 806ea3f..7573b7b 100644
--- a/src/app/upload/components/OutlineSection/OutlineSection.tsx
+++ b/src/app/upload/components/OutlineSection/OutlineSection.tsx
@@ -1,23 +1,23 @@
import { useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
+import { disabledInput, uploadInputContainer } from './OutlineSection.styles';
import { countSelectOptions, durationMinutesOptions } from '../../upload.constants';
import CheckboxWithIcon from '../CheckboxWithIcon/CheckboxWithIcon';
import InputForm from '../InputForm/InputForm';
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup';
import RegionPopover from '../RegionPopover/RegionPopover';
import SelectForm from '../SelectForm/SelectForm';
-
-import DatePickerForm from '@/app/upload/components/DatePickerForm/DatePickerForm';
-import { colors } from '@/styles/colors';
-import { MatchType } from '@/types/uploadExperimentPost';
import {
headingIcon,
label,
uploadInput,
outlineFormLayout,
} from '../UploadContainer/UploadContainer.styles';
-import { disabledInput, uploadInputContainer } from './OutlineSection.styles';
+
+import DatePickerForm from '@/app/upload/components/DatePickerForm/DatePickerForm';
+import { colors } from '@/styles/colors';
+import { MatchType } from '@/types/uploadExperimentPost';
const OutlineSection = () => {
const { control, setValue } = useFormContext();
diff --git a/src/app/upload/components/RegionPopover/RegionPopover.tsx b/src/app/upload/components/RegionPopover/RegionPopover.tsx
index 1a9cde6..45082c7 100644
--- a/src/app/upload/components/RegionPopover/RegionPopover.tsx
+++ b/src/app/upload/components/RegionPopover/RegionPopover.tsx
@@ -1,6 +1,6 @@
import * as Popover from '@radix-ui/react-popover';
-import { FieldError } from 'react-hook-form';
import { useRef } from 'react';
+import { FieldError } from 'react-hook-form';
import {
regionField,
@@ -14,10 +14,10 @@ import {
placeholderText,
regionPopoverContainer,
} from './RegionPopover.styles';
+import { uploadInput } from '../UploadContainer/UploadContainer.styles';
import Icon from '@/components/Icon';
import { UPLOAD_REGION } from '@/constants/uploadRegion';
-import { uploadInput } from '../UploadContainer/UploadContainer.styles';
interface RegionPopoverProps {
regionPopoverProps: {
diff --git a/src/app/upload/components/UploadContainer/UploadContainer.tsx b/src/app/upload/components/UploadContainer/UploadContainer.tsx
index a46b5fa..b9f7bc1 100644
--- a/src/app/upload/components/UploadContainer/UploadContainer.tsx
+++ b/src/app/upload/components/UploadContainer/UploadContainer.tsx
@@ -4,10 +4,6 @@ import Link from 'next/link';
import React, { useState } from 'react';
import { FormProvider } from 'react-hook-form';
-import useUploadExperimentPost from '../../hooks/useUploadExperimentPost';
-import ApplyMethodSection from '../ApplyMethodSection/ApplyMethodSection';
-import DescriptionSection from '../DescriptionSection/DescriptionSection';
-import OutlineSection from '../OutlineSection/OutlineSection';
import {
activeButton,
buttonContainer,
@@ -16,6 +12,10 @@ import {
uploadContentLayout,
uploadLayout,
} from './UploadContainer.styles';
+import useUploadExperimentPost from '../../hooks/useUploadExperimentPost';
+import ApplyMethodSection from '../ApplyMethodSection/ApplyMethodSection';
+import DescriptionSection from '../DescriptionSection/DescriptionSection';
+import OutlineSection from '../OutlineSection/OutlineSection';
const UploadContainer = () => {
const [addLink, setAddLink] = useState(false);
From e28b3878d204f74efb68239f3dede39a9c8e2a57 Mon Sep 17 00:00:00 2001
From: eeeyooon
Date: Sat, 25 Jan 2025 03:50:18 +0900
Subject: [PATCH 32/32] =?UTF-8?q?[YS-172]=20feat:=20=EA=B3=B5=EA=B3=A0=20?=
=?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=ED=86=A0?=
=?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=95=8C=EB=A6=BC=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../UploadContainer/UploadContainer.tsx | 28 ++++++++++++++++-
.../upload/hooks/useUploadExperimentPost.tsx | 30 +++++++++++--------
2 files changed, 45 insertions(+), 13 deletions(-)
diff --git a/src/app/upload/components/UploadContainer/UploadContainer.tsx b/src/app/upload/components/UploadContainer/UploadContainer.tsx
index b9f7bc1..7eaa5d7 100644
--- a/src/app/upload/components/UploadContainer/UploadContainer.tsx
+++ b/src/app/upload/components/UploadContainer/UploadContainer.tsx
@@ -1,5 +1,6 @@
'use client';
+import * as Toast from '@radix-ui/react-toast';
import Link from 'next/link';
import React, { useState } from 'react';
import { FormProvider } from 'react-hook-form';
@@ -17,11 +18,25 @@ import ApplyMethodSection from '../ApplyMethodSection/ApplyMethodSection';
import DescriptionSection from '../DescriptionSection/DescriptionSection';
import OutlineSection from '../OutlineSection/OutlineSection';
+import {
+ toastLayout,
+ toastTitle,
+ toastViewport,
+} from '@/app/post/[post_id]/components/ParticipationGuideModal/ParticipationGuideModal.styles';
+import Icon from '@/components/Icon';
+import { colors } from '@/styles/colors';
+
const UploadContainer = () => {
const [addLink, setAddLink] = useState(false);
const [addContact, setAddContact] = useState(false);
- const { form, handleSubmit } = useUploadExperimentPost({ addLink, addContact });
+ const [openToast, setOpenToast] = useState(false);
+
+ const { form, handleSubmit } = useUploadExperimentPost({
+ addLink,
+ addContact,
+ setOpenToast,
+ });
return (
@@ -57,6 +72,17 @@ const UploadContainer = () => {
+
+ {/* 공고 등록 실패 시 토스트 알림 */}
+
+
+
+
+ 공고 등록을 실패하였습니다.
+
+
+
+
);
};
diff --git a/src/app/upload/hooks/useUploadExperimentPost.tsx b/src/app/upload/hooks/useUploadExperimentPost.tsx
index df9987c..4d1ed01 100644
--- a/src/app/upload/hooks/useUploadExperimentPost.tsx
+++ b/src/app/upload/hooks/useUploadExperimentPost.tsx
@@ -1,20 +1,26 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
+import { Dispatch, SetStateAction } from 'react';
import { useForm } from 'react-hook-form';
import { convertLabelToValue } from '../upload.utils';
+import useUploadExperimentPostAPI from '@/apis/hooks/useUploadExperimentPostAPI';
import UploadExperimentPostSchema, {
UploadExperimentPostSchemaType,
} from '@/schema/upload/uploadExperimentPostSchema';
-import useUploadExperimentPostAPI from '@/apis/hooks/useUploadExperimentPostAPI';
interface useUploadExperimentPostProps {
addLink: boolean;
addContact: boolean;
+ setOpenToast: Dispatch
>;
}
-const useUploadExperimentPost = ({ addLink, addContact }: useUploadExperimentPostProps) => {
+const useUploadExperimentPost = ({
+ addLink,
+ addContact,
+ setOpenToast,
+}: useUploadExperimentPostProps) => {
const router = useRouter();
const form = useForm({
@@ -53,7 +59,6 @@ const useUploadExperimentPost = ({ addLink, addContact }: useUploadExperimentPos
},
});
- /* 공고 등록 API */
const { mutateAsync: uploadExperimentPost } = useUploadExperimentPostAPI();
const handleSubmit = async (data: UploadExperimentPostSchemaType) => {
@@ -62,15 +67,16 @@ const useUploadExperimentPost = ({ addLink, addContact }: useUploadExperimentPos
area: data.area ? convertLabelToValue(data.area) : undefined,
};
- uploadExperimentPost(updatedData, {
- onSuccess: (response) => {
- form.reset();
- router.push(`/post/${response.postInfo.experimentPostId}`);
- },
- onError: (error) => {
- console.error('공고 등록 form 저장 중 오류 발생', error);
- },
- });
+ try {
+ await uploadExperimentPost(updatedData, {
+ onSuccess: (response) => {
+ form.reset();
+ router.push(`/post/${response.postInfo.experimentPostId}`);
+ },
+ });
+ } catch {
+ setOpenToast(true);
+ }
};
return {