diff --git a/package-lock.json b/package-lock.json index e859003b..3f730e15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30142,6 +30142,126 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", + "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/packages/client/app/(withLayout)/question/create/page.tsx b/packages/client/app/(withLayout)/question/create/page.tsx index 5feb9574..eb580f57 100644 --- a/packages/client/app/(withLayout)/question/create/page.tsx +++ b/packages/client/app/(withLayout)/question/create/page.tsx @@ -1,9 +1,10 @@ import { QuestionCreateInputs } from '@widgets/QuestionCreateInputs' +import { ClientQuestionCreatePage } from 'src/pages/questionCreate' export default function QuestionCreatePage() { return (
- +
) } diff --git a/packages/client/src/entities/question/index.ts b/packages/client/src/entities/question/index.ts new file mode 100644 index 00000000..a7f29361 --- /dev/null +++ b/packages/client/src/entities/question/index.ts @@ -0,0 +1,3 @@ +export { useCreateQuestionForm } from './lib/useQuestionCreateForm' + +export type { QuestionFormValues } from './lib/useQuestionCreateForm' diff --git a/packages/client/src/entities/question/lib/useQuestionCreateForm.ts b/packages/client/src/entities/question/lib/useQuestionCreateForm.ts new file mode 100644 index 00000000..03b93887 --- /dev/null +++ b/packages/client/src/entities/question/lib/useQuestionCreateForm.ts @@ -0,0 +1,21 @@ +import { useForm, UseFormReturn } from 'react-hook-form' + +export type QuestionFormValues = { + title: string + content: string + jobCategory: null | { label: string; id: string } + reward: number +} + +export const useCreateQuestionForm = (): UseFormReturn => { + const form = useForm({ + mode: 'onTouched', + defaultValues: { + title: '', + content: '', + jobCategory: null, + reward: 1000, + }, + }) + return form as UseFormReturn +} diff --git a/packages/client/src/entities/question/lib/useValidateQuestion.ts b/packages/client/src/entities/question/lib/useValidateQuestion.ts new file mode 100644 index 00000000..9e5b2c7e --- /dev/null +++ b/packages/client/src/entities/question/lib/useValidateQuestion.ts @@ -0,0 +1,55 @@ +import { UseFormReturn, useWatch } from 'react-hook-form' +import { QuestionFormValues } from './useQuestionCreateForm' + +export const useValidateQuestion = ( + form: UseFormReturn, +) => { + const { title, content, jobCategory, reward } = useWatch({ + control: form.control, + }) + + const isTitleValid = !!title && title?.length > 0 && title?.length <= 20 + const isContentValid = + !!content && content?.length > 0 && content?.length <= 500 + const isCategoryValid = + jobCategory !== null && + !!jobCategory?.id && + !!jobCategory?.label && + typeof jobCategory?.id === 'string' && + typeof jobCategory?.label === 'string' + const isRewardValid = !!reward && reward >= 1000 && reward <= 100000 + + const getTitleErrorMessage = () => { + if (!isTitleValid) { + return '제목은 1자 이상 20자 이하로 작성해주세요' + } + return '' + } + const getContentErrorMessage = () => { + if (!isContentValid) { + return '내용은 1자 이상 500자 이하로 작성해주세요' + } + return '' + } + const getJobCategoryErrorMessage = () => { + if (!isCategoryValid) { + return '직군을 선택해주세요' + } + return '' + } + const getRewardErrorMessage = () => { + if (!isRewardValid) { + return '보상은 1,000원 이상 10,000원 이하로 설정해주세요' + } + return '' + } + + return { + getTitleErrorMessage, + getContentErrorMessage, + getJobCategoryErrorMessage, + getRewardErrorMessage, + canSubmit: + isTitleValid && isContentValid && isCategoryValid && isRewardValid, + } +} diff --git a/packages/client/src/pages/questionCreate/index.ts b/packages/client/src/pages/questionCreate/index.ts new file mode 100644 index 00000000..817caddb --- /dev/null +++ b/packages/client/src/pages/questionCreate/index.ts @@ -0,0 +1 @@ +export { ClientQuestionCreatePage } from './ui/Page' diff --git a/packages/client/src/pages/questionCreate/ui/Page.tsx b/packages/client/src/pages/questionCreate/ui/Page.tsx new file mode 100644 index 00000000..70af32f6 --- /dev/null +++ b/packages/client/src/pages/questionCreate/ui/Page.tsx @@ -0,0 +1,16 @@ +'use client' + +import { useCreateQuestionForm } from '@entities/question' +import { QuestionCreateInputs } from '@widgets/QuestionCreateInputs' + +export function ClientQuestionCreatePage() { + const form = useCreateQuestionForm() + // api 연동시 onSubmit 함수 구현 + + return ( + alert('TODO: submit')} + /> + ) +} diff --git a/packages/client/src/pages/readme.md b/packages/client/src/pages/readme.md new file mode 100644 index 00000000..5a7bc4ec --- /dev/null +++ b/packages/client/src/pages/readme.md @@ -0,0 +1,22 @@ +# pages of FSD Architecture for client render + +- use app/page.tsx or app/(withLayout)/\*/page.tsx of app router +- the 2nd highest layer of FSD layers +- application pages (e.g. /signin , /home) +- can import from all layers (only form index files in each folder for encapsulation) +- a little bit different from pages from FSD + +# Layer priorities + +1. apps (same as app in the FSD) +2. pages ✅ +3. widgets +4. features +5. entities +6. shared + +# Reference + +- [FSD official docs](https://feature-sliced.design/docs) +- [ko tech article](https://emewjin.github.io/feature-sliced-design/) +- [example](https://nukeapp.netlify.app/) diff --git a/packages/client/src/widgets/QuestionCreateInputs/ui/QuestionCreateInputs.tsx b/packages/client/src/widgets/QuestionCreateInputs/ui/QuestionCreateInputs.tsx index 4ed28383..c1d05934 100644 --- a/packages/client/src/widgets/QuestionCreateInputs/ui/QuestionCreateInputs.tsx +++ b/packages/client/src/widgets/QuestionCreateInputs/ui/QuestionCreateInputs.tsx @@ -1,57 +1,132 @@ 'use client' +import { QuestionFormValues } from '@entities/question' +import { useValidateQuestion } from '@entities/question/lib/useValidateQuestion' import { Button, - TextArea, - TextInput, IconButton, - Select, NumberInput, + Select, + TextArea, + TextInput, } from '@gds/component' import { IconAddPhoto, IconSearch } from '@gds/icon' +import { Controller, UseFormReturn } from 'react-hook-form' import { pageWrapper } from './questionCreateInputs.css' -export function QuestionCreateInputs() { +interface Props { + form: UseFormReturn + onSubmit: () => void +} + +export function QuestionCreateInputs({ form, onSubmit }: Props) { + const { + canSubmit, + getTitleErrorMessage, + getJobCategoryErrorMessage, + getContentErrorMessage, + getRewardErrorMessage, + } = useValidateQuestion(form) return (
- -