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 (
-
-