diff --git a/src/entities/projects/ui/project-insert/ProjectDetailDescriptionCard.tsx b/src/entities/projects/ui/project-insert/ProjectDetailDescriptionCard.tsx new file mode 100644 index 0000000..a54bd29 --- /dev/null +++ b/src/entities/projects/ui/project-insert/ProjectDetailDescriptionCard.tsx @@ -0,0 +1,79 @@ +import { TextField } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import type { ChangeEvent, CSSProperties, JSX } from "react"; + +import SimpleFormCard from "@shared/ui/project-insert/SimpleFormCard"; + +interface ProjectDetailDescriptionCardProps { + value: string; + onChange: (value: string) => void; + large?: boolean; + style?: CSSProperties; +} + +const ProjectDetailDescriptionCard = ({ + value, + onChange, + large, + style, +}: ProjectDetailDescriptionCardProps): JSX.Element => { + const theme = useTheme(); + + const handleChange = (e: ChangeEvent): void => { + onChange(e.target.value); + }; + + return ( + + + + ); +}; + +export default ProjectDetailDescriptionCard; diff --git a/src/entities/projects/ui/project-insert/ProjectPreferentialCard.tsx b/src/entities/projects/ui/project-insert/ProjectPreferentialCard.tsx new file mode 100644 index 0000000..7ac20ce --- /dev/null +++ b/src/entities/projects/ui/project-insert/ProjectPreferentialCard.tsx @@ -0,0 +1,154 @@ +import AddIcon from "@mui/icons-material/Add"; +import { Box, Button } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import type { + ChangeEvent, + CSSProperties, + JSX, + FocusEvent, + MouseEvent, +} from "react"; +import { useState } from "react"; + +import SimpleFormCard from "@shared/ui/project-insert/SimpleFormCard"; + +interface ProjectPreferentialCardProps { + value: string[]; + onChange: (value: string[]) => void; + large?: boolean; + style?: CSSProperties; +} + +const ProjectPreferentialCard = ({ + value, + onChange, + large, + style, +}: ProjectPreferentialCardProps): JSX.Element => { + const theme = useTheme(); + const [newPreferential, setNewPreferential] = useState(""); + + const addPreferential = (): void => { + if (newPreferential.trim() && !value.includes(newPreferential.trim())) { + onChange([...value, newPreferential.trim()]); + setNewPreferential(""); + } + }; + + const removePreferential = (preferentialToRemove: string): void => { + onChange(value.filter((pref) => pref !== preferentialToRemove)); + }; + + return ( + + {/* 입력 + 추가 버튼 */} + + ) => + setNewPreferential(e.target.value) + } + placeholder="예: AWS, Docker, 스타트업 경험..." + style={{ + flex: 1, + height: 40, + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + fontSize: large + ? theme.typography.h5.fontSize + : theme.typography.body1.fontSize, + fontFamily: theme.typography.fontFamily, + background: theme.palette.background.paper, + padding: large ? theme.spacing(2.2) : theme.spacing(1.7), + boxSizing: "border-box", + outline: "none", + transition: "border-color 0.2s ease-in-out", + }} + onFocus={(e) => { + e.target.style.borderColor = theme.palette.primary.main; + e.target.style.borderWidth = "2px"; + }} + onBlur={(e: FocusEvent) => { + e.currentTarget.style.borderColor = theme.palette.divider; + e.currentTarget.style.borderWidth = "1px"; + }} + onMouseEnter={(e: MouseEvent) => { + if (e.currentTarget !== document.activeElement) { + e.currentTarget.style.borderColor = "#000000"; + } + }} + onMouseLeave={(e: MouseEvent) => { + if (e.currentTarget !== document.activeElement) { + e.currentTarget.style.borderColor = theme.palette.divider; + } + }} + /> + + + + {/* 우대사항 태그들 */} + {value.length > 0 && ( + + {value.map((preferential, index) => ( + + {preferential} + removePreferential(preferential)} + sx={{ + width: 18, + height: 18, + borderRadius: "100%", + border: "none", + color: theme.palette.text.primary, + backgroundColor: "transparent", + cursor: "pointer", + fontSize: "18px", + display: "flex", + alignItems: "center", + justifyContent: "center", + }} + > + × + + + ))} + + )} + + ); +}; + +export default ProjectPreferentialCard; diff --git a/src/entities/projects/ui/project-insert/ProjectRequirementsCard.tsx b/src/entities/projects/ui/project-insert/ProjectRequirementsCard.tsx new file mode 100644 index 0000000..2a827b8 --- /dev/null +++ b/src/entities/projects/ui/project-insert/ProjectRequirementsCard.tsx @@ -0,0 +1,141 @@ +import AddIcon from "@mui/icons-material/Add"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { Box, IconButton, TextField, Button } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import type { ChangeEvent, CSSProperties, JSX } from "react"; + +import SimpleFormCard from "@shared/ui/project-insert/SimpleFormCard"; + +interface ProjectRequirementsCardProps { + value: string[]; + onChange: (value: string[]) => void; + large?: boolean; + style?: CSSProperties; +} + +const ProjectRequirementsCard = ({ + value, + onChange, + large, + style, +}: ProjectRequirementsCardProps): JSX.Element => { + const theme = useTheme(); + + const displayValue = value.length === 0 ? [""] : value; + + const handleInputChange = (index: number, newValue: string): void => { + const newArr = [...displayValue]; + newArr[index] = newValue; + onChange(newArr); + }; + + const addInput = (): void => { + onChange([...displayValue, ""]); + }; + + const removeInput = (index: number): void => { + if (displayValue.length <= 1) return; + const newArr = displayValue.filter((_, i) => i !== index); + onChange(newArr); + }; + + return ( + + + {displayValue.map((requirement, index) => ( + + ) => + handleInputChange(index, e.target.value) + } + placeholder="예: 주 2-3회 온라인 미팅 참여 가능" + fullWidth + variant="outlined" + size="small" + sx={{ + "& .MuiOutlinedInput-root": { + height: 40, + fontSize: large + ? theme.typography.h5.fontSize + : theme.typography.body1.fontSize, + fontFamily: theme.typography.fontFamily, + background: "none", + border: "none", + + // 호버 시 테두리 검정색 + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.text.primary, + }, + // 포커스 시 테두리 primary 색상 + "&.Mui-focused .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.primary.main, + borderWidth: "2px", + }, + }, + "& .MuiOutlinedInput-input": { + padding: large ? theme.spacing(2.2) : theme.spacing(1.7), + }, + }} + /> + removeInput(index)} + color="error" + size="small" + disabled={displayValue.length <= 1} + sx={{ + ml: 0.5, + "&:hover": { + backgroundColor: "transparent", + }, + }} + > + + + + ))} + + + + + + ); +}; + +export default ProjectRequirementsCard; diff --git a/src/entities/projects/ui/project-insert/ProjectScheduleManagementCard.tsx b/src/entities/projects/ui/project-insert/ProjectScheduleManagementCard.tsx new file mode 100644 index 0000000..24c52c1 --- /dev/null +++ b/src/entities/projects/ui/project-insert/ProjectScheduleManagementCard.tsx @@ -0,0 +1,224 @@ +import AddIcon from "@mui/icons-material/Add"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { + Box, + Button, + FormControl, + IconButton, + InputLabel, + MenuItem, + Select, + TextField, + useMediaQuery, + useTheme, +} from "@mui/material"; +import type { SelectChangeEvent } from "@mui/material"; +import type { CSSProperties, JSX, ChangeEvent } from "react"; + +import type { ProjectSchedule } from "@shared/types/schedule"; +import { ExpectedPeriod } from "@shared/types/schedule"; +import SimpleFormCard from "@shared/ui/project-insert/SimpleFormCard"; + +interface ProjectScheduleManagementCardProps { + value: ProjectSchedule[]; + onChange: (value: ProjectSchedule[]) => void; + large?: boolean; + style?: CSSProperties; +} + +const PERIOD_OPTIONS = [ + { value: ExpectedPeriod.oneMonth, label: "1개월" }, + { value: ExpectedPeriod.twoMonths, label: "2개월" }, + { value: ExpectedPeriod.threeMonths, label: "3개월" }, + { value: ExpectedPeriod.fourMonths, label: "4개월" }, + { value: ExpectedPeriod.sixMonths, label: "6개월" }, + { value: ExpectedPeriod.moreThanSixMonths, label: "6개월 이상" }, +]; + +const INIT_SCHEDULE: ProjectSchedule = { + stageName: "", + period: ExpectedPeriod.oneMonth, + description: "", +}; + +const ProjectScheduleManagementCard = ({ + value, + onChange, + large, + style, +}: ProjectScheduleManagementCardProps): JSX.Element => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + + // 비어있을 때 기본 일정 1개 추가 + const schedules = value.length === 0 ? [INIT_SCHEDULE] : value; + + // 일정 추가 + const addSchedule = (): void => { + const newSchedule: ProjectSchedule = { ...INIT_SCHEDULE }; + onChange([...value, newSchedule]); + }; + + // 일정 삭제 + const removeSchedule = (index: number): void => { + const currentSchedules = value.length === 0 ? [INIT_SCHEDULE] : value; + const newSchedules = currentSchedules.filter((_, i) => i !== index); + onChange(newSchedules); + }; + + // 일정 수정 + const updateSchedule = ( + index: number, + field: keyof ProjectSchedule, + newValue: string | ExpectedPeriod + ): void => { + const currentSchedules = value.length === 0 ? [INIT_SCHEDULE] : value; + const newSchedules = [...currentSchedules]; + + if (index < newSchedules.length) { + newSchedules[index] = { ...newSchedules[index], [field]: newValue }; + onChange(newSchedules); + } + }; + + return ( + + + {/* 일정 목록 */} + + {schedules.map((schedule, index) => ( + + {/* 단계명 */} + ) => + updateSchedule(index, "stageName", e.target.value) + } + fullWidth + sx={{ + "& .MuiOutlinedInput-root": { + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.text.primary, + }, + "&.Mui-focused .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.primary.main, + borderWidth: "2px", + }, + }, + }} + /> + + {/* 기간 선택 */} + + 기간 + + value={schedule.period} + onChange={(e: SelectChangeEvent) => + updateSchedule( + index, + "period", + e.target.value as ExpectedPeriod + ) + } + label="기간" + > + {PERIOD_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + + {/* 설명 */} + ) => + updateSchedule(index, "description", e.target.value) + } + fullWidth + sx={{ + "& .MuiOutlinedInput-root": { + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.text.primary, + }, + "&.Mui-focused .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.primary.main, + borderWidth: "2px", + }, + }, + }} + /> + + {/* 삭제 버튼 */} + removeSchedule(index)} + color="error" + size="small" + disabled={schedules.length <= 1} + sx={{ + "&:hover": { + backgroundColor: "transparent", + }, + }} + > + + + + ))} + + + {/* 일정 추가 버튼 */} + + + + + + ); +}; + +export default ProjectScheduleManagementCard; diff --git a/src/entities/projects/ui/project-insert/ProjectTechStackCard.tsx b/src/entities/projects/ui/project-insert/ProjectTechStackCard.tsx index 8895a2b..f410aaa 100644 --- a/src/entities/projects/ui/project-insert/ProjectTechStackCard.tsx +++ b/src/entities/projects/ui/project-insert/ProjectTechStackCard.tsx @@ -106,7 +106,7 @@ const ProjectTechStackCard = ({ disabled={!newTech.trim()} sx={{ minWidth: 48, - height: 40, + height: 46, backgroundColor: "#2563EB", "&:hover": { backgroundColor: "#1d4ed8" }, }} diff --git a/src/entities/projects/ui/project-insert/ProjectWorkflowCard.tsx b/src/entities/projects/ui/project-insert/ProjectWorkflowCard.tsx new file mode 100644 index 0000000..72a1285 --- /dev/null +++ b/src/entities/projects/ui/project-insert/ProjectWorkflowCard.tsx @@ -0,0 +1,92 @@ +import { FormControl, Select, MenuItem } from "@mui/material"; +import type { SelectChangeEvent } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import type { CSSProperties, JSX } from "react"; + +import { Workflow } from "@shared/types/project"; +import SimpleFormCard from "@shared/ui/project-insert/SimpleFormCard"; + +interface ProjectWorkflowCardProps { + value: Workflow | ""; + onChange: (value: Workflow) => void; + large?: boolean; + style?: CSSProperties; +} + +const WORKFLOW_OPTIONS = [ + { + value: Workflow.online, + label: "온라인 (원격)", + }, + { + value: Workflow.offlineInSeoul, + label: "오프라인 (서울)", + }, + { + value: Workflow.offlineInBusan, + label: "오프라인 (부산)", + }, + { + value: Workflow.offlineInAnywhere, + label: "오프라인 (자유)", + }, + { + value: Workflow.hybrid, + label: "하이브리드 (온라인 + 오프라인)", + }, +]; + +export default function ProjectWorkflowCard({ + value, + onChange, + large = false, + style, +}: ProjectWorkflowCardProps): JSX.Element { + const theme = useTheme(); + + const handleChange = (event: SelectChangeEvent): void => { + onChange(event.target.value as Workflow); + }; + + return ( + + + + value={value || ("" as Workflow)} + onChange={handleChange} + size={large ? "medium" : "small"} + displayEmpty + sx={{ + fontSize: large + ? theme.typography.h5.fontSize + : theme.typography.body1.fontSize, + fontFamily: theme.typography.fontFamily, + padding: large ? theme.spacing(2.2) : theme.spacing(1.7), + + height: 40, + "& .MuiSelect-select": { + height: "40px", + display: "flex", + alignItems: "center", + padding: 0, + }, + }} + > + + {WORKFLOW_OPTIONS.map((option) => ( + +
{option.label}
+
+ ))} + +
+
+ ); +} diff --git a/src/features/projects/hook/useInsertStep3.ts b/src/features/projects/hook/useInsertStep3.ts new file mode 100644 index 0000000..4d631e6 --- /dev/null +++ b/src/features/projects/hook/useInsertStep3.ts @@ -0,0 +1,100 @@ +import { useState, type ChangeEvent } from "react"; + +import { type ProjectItemInsertReq } from "@shared/types/project"; +import { ExpectedPeriod } from "@shared/types/schedule"; + +// Schedule 타입 정의 (원본 코드 참고) +interface Schedule { + stageName: string; + period: ExpectedPeriod; + description: string; +} + +type Step3Type = Pick; + +interface ApplyFormResult { + form3: Step3Type; + setting: { + // 내부 Input 셋팅용 + scheduleItem: ( + index: number, + field: K, + value: Schedule[K] + ) => void; + }; + update: { + // form 업데이트용 + description: (e: ChangeEvent) => void; + newSchedule: () => void; + removeSchedule: (idx: number) => void; + }; +} + +const useInsertStep3 = ({ state }: { state?: Step3Type }): ApplyFormResult => { + const isModify = !!state; // 추후에 수정을 위해서 + + const [form3, setForm3] = useState(isModify ? state : initForm3); + + // 프로젝트 설명 + const updateDescription = (e: ChangeEvent): void => { + setForm3((prev) => ({ + ...prev, + description: e.target.value, + })); + }; + + // 일정 수정 + const updateScheduleField = ( + index: number, + field: K, + value: Schedule[K] + ): void => { + setForm3((prev) => ({ + ...prev, + schedules: prev.schedules.map((schedule, i) => + i === index ? { ...schedule, [field]: value } : schedule + ), + })); + }; + + // 새 일정 추가 + const insertNewSchedule = (): void => { + setForm3((prev) => ({ + ...prev, + schedules: [...prev.schedules, initSchedule], + })); + }; + + // 일정 삭제 + const removeSchedule = (idx: number): void => { + setForm3((prev) => ({ + ...prev, + schedules: prev.schedules.filter((_, i) => i !== idx), + })); + }; + + return { + form3, + setting: { + scheduleItem: updateScheduleField, + }, + update: { + description: updateDescription, + newSchedule: insertNewSchedule, + removeSchedule: removeSchedule, + }, + }; +}; + +export default useInsertStep3; + +const initForm3: Step3Type = { + description: "", + schedules: [], +}; + +const initSchedule: Schedule = { + stageName: "", + period: ExpectedPeriod.oneMonth, + description: "", +}; diff --git a/src/features/projects/hook/useInsertStep4.ts b/src/features/projects/hook/useInsertStep4.ts new file mode 100644 index 0000000..e45fbd1 --- /dev/null +++ b/src/features/projects/hook/useInsertStep4.ts @@ -0,0 +1,121 @@ +import { useState, type ChangeEvent } from "react"; + +import { type ProjectItemInsertReq, Workflow } from "@shared/types/project"; + +type Step4Type = Pick< + ProjectItemInsertReq, + "workflow" | "requirements" | "preferentialTreatment" +>; + +interface ApplyFormResult { + form4: Step4Type; + setting: { + // 내부 Input 셋팅용 + requirementItem: (e: ChangeEvent) => void; + preferentialItem: (e: ChangeEvent) => void; + }; + update: { + // form 업데이트용 + workflow: (e: ChangeEvent) => void; + requirements: () => void; + removeRequirement: (idx: number) => void; + preferentialTreatment: () => void; + removePreferentialTreatment: (idx: number) => void; + }; +} + +const useInsertStep4 = ({ state }: { state?: Step4Type }): ApplyFormResult => { + const isModify = !!state; // 추후에 수정을 위해서 + + const [form4, setForm4] = useState(isModify ? state : initForm4); + const [requirementItem, setRequirementItem] = useState(""); + const [preferentialItem, setPreferentialItem] = useState(""); + + // 진행 방식 + const updateWorkflow = (e: ChangeEvent): void => { + const value = e.target.value as keyof typeof Workflow; + + if (value in Workflow) { + setForm4((prev) => ({ + ...prev, + workflow: Workflow[value], + })); + } + }; + + // 지원 요건 input box 용 + const settingRequirementItem = (e: ChangeEvent): void => { + setRequirementItem(e.target.value); + }; + + // 지원 요건 + 버튼 시 list에 추가 + const updateRequirements = (): void => { + if (requirementItem.trim()) { + setForm4((prev) => ({ + ...prev, + requirements: [...prev.requirements, requirementItem.trim()], + })); + setRequirementItem(""); // 입력 필드 초기화 + } + }; + + // 지원 요건 삭제 + const removeRequirement = (idx: number): void => { + setForm4((prev) => ({ + ...prev, + requirements: prev.requirements.filter((_, i) => i !== idx), + })); + }; + + // 우대 사항 input box 용 + const settingPreferentialItem = (e: ChangeEvent): void => { + setPreferentialItem(e.target.value); + }; + + // 우대 사항 + 버튼 시 list에 추가 + const updatePreferentialTreatment = (): void => { + if (preferentialItem.trim()) { + setForm4((prev) => ({ + ...prev, + preferentialTreatment: [ + ...prev.preferentialTreatment, + preferentialItem.trim(), + ], + })); + setPreferentialItem(""); // 입력 필드 초기화 + } + }; + + // 우대 사항 삭제 + const removePreferentialTreatment = (idx: number): void => { + setForm4((prev) => ({ + ...prev, + preferentialTreatment: prev.preferentialTreatment.filter( + (_, i) => i !== idx + ), + })); + }; + + return { + form4, + setting: { + requirementItem: settingRequirementItem, + preferentialItem: settingPreferentialItem, + }, + update: { + workflow: updateWorkflow, + requirements: updateRequirements, + removeRequirement: removeRequirement, + preferentialTreatment: updatePreferentialTreatment, + removePreferentialTreatment: removePreferentialTreatment, + }, + }; +}; + +export default useInsertStep4; + +const initForm4: Step4Type = { + workflow: Workflow.online, + requirements: [], + preferentialTreatment: [], +}; diff --git a/src/features/projects/hook/useProjectInsertForm.ts b/src/features/projects/hook/useProjectInsertForm.ts index 2c7a37f..cf41860 100644 --- a/src/features/projects/hook/useProjectInsertForm.ts +++ b/src/features/projects/hook/useProjectInsertForm.ts @@ -25,11 +25,18 @@ export type Step2Type = Pick< > & { expectedPeriod: ExpectedPeriod | ""; }; +export type Step3Type = Pick; +export type Step4Type = Pick< + ProjectItemInsertReq, + "workflow" | "requirements" | "preferentialTreatment" +>; interface InsertFormResult { form: { step1: Setp1Type; step2: Step2Type; + step3: Step3Type; + step4: Step4Type; }; page: { currentStep: number; @@ -38,7 +45,9 @@ interface InsertFormResult { }; submit: () => Promise; onChange: { - step2: (field: keyof Step2Type, value: any) => void; + step2: (field: keyof Step2Type, value: Step2Type[keyof Step2Type]) => void; + step3: (field: keyof Step3Type, value: Step3Type[keyof Step3Type]) => void; + step4: (field: keyof Step4Type, value: Step4Type[keyof Step4Type]) => void; }; } @@ -49,11 +58,31 @@ const useProjectInsertForm = (): InsertFormResult => { const [currentStep, setCurrentStep] = useState(1); // const [formStep1, setFormStep1] = useState(initForm1); - const [formStep2, setFormStep2] = useState(initForm2); + // Step2 상태 + const [formStep2, setFormStep2] = useState({ + teamSize: 0, + expectedPeriod: "", + techStack: [], + positions: [], + }); + const [formStep3, setFormStep3] = useState(initForm3); + const [formStep4, setFormStep4] = useState(initForm4); const handleChangeStep2 = (field: keyof Step2Type, value: any): void => { setFormStep2((prev) => ({ ...prev, [field]: value })); }; + const handleChangeStep3 = ( + field: keyof Step3Type, + value: Step3Type[keyof Step3Type] + ): void => { + setFormStep3((prev) => ({ ...prev, [field]: value })); + }; + const handleChangeStep4 = ( + field: keyof Step4Type, + value: Step4Type[keyof Step4Type] + ): void => { + setFormStep4((prev) => ({ ...prev, [field]: value })); + }; const handlePrev = (): void => { setCurrentStep((prev) => prev - 1); @@ -83,6 +112,8 @@ const useProjectInsertForm = (): InsertFormResult => { form: { step1: initForm1, step2: formStep2, + step3: formStep3, + step4: formStep4, }, page: { currentStep: currentStep, @@ -92,6 +123,8 @@ const useProjectInsertForm = (): InsertFormResult => { submit, onChange: { step2: handleChangeStep2, + step3: handleChangeStep3, + step4: handleChangeStep4, }, }; }; diff --git a/src/features/projects/ui/project-insert/Step3.tsx b/src/features/projects/ui/project-insert/Step3.tsx index 3c3bc43..d54f9d9 100644 --- a/src/features/projects/ui/project-insert/Step3.tsx +++ b/src/features/projects/ui/project-insert/Step3.tsx @@ -1,20 +1,60 @@ -import { Box, styled } from "@mui/material"; +import { Box, styled, useMediaQuery, useTheme } from "@mui/material"; import type { JSX } from "react"; -const Step3 = (): JSX.Element => { - return Step3; +import type { Step3Type } from "@features/projects/hook/useProjectInsertForm"; + +import ProjectDetailDescriptionCard from "@entities/projects/ui/project-insert/ProjectDetailDescriptionCard"; +import ProjectScheduleManagementCard from "@entities/projects/ui/project-insert/ProjectScheduleManagementCard"; + +import type { ExpectedPeriod } from "@shared/types/schedule"; + +interface Schedule { + stageName: string; + period: ExpectedPeriod; // ExpectedPeriod enum 값 + description: string; +} + +interface Step3Props { + form: Step3Type; + onChangeForm: ( + field: keyof Step3Type, + value: Step3Type[keyof Step3Type] + ) => void; +} + +const Step3 = ({ form, onChangeForm }: Step3Props): JSX.Element => { + const theme = useTheme(); + const isMdDown = useMediaQuery(theme.breakpoints.down("md")); + + return ( + + onChangeForm("description", value)} + large + style={{ gridColumn: isMdDown ? "span 1" : "1 / -1" }} + /> + + onChangeForm("schedules", value)} + large + style={{ gridColumn: isMdDown ? "span 1" : "1 / -1" }} + /> + + ); }; export default Step3; export const StepBox = styled(Box)(({ theme }) => ({ display: "grid", - gridTemplateColumns: "1fr", // 기본값(xs) - gap: theme.spacing(2), // 기본값(sm 이하) + gridTemplateColumns: "1fr", + gap: theme.spacing(2), marginBottom: 0, [theme.breakpoints.up("md")]: { gridTemplateColumns: "1fr 1fr", - gap: theme.spacing(4), + gap: theme.spacing(3), }, })); diff --git a/src/features/projects/ui/project-insert/Step4.tsx b/src/features/projects/ui/project-insert/Step4.tsx index b03c4e8..c242306 100644 --- a/src/features/projects/ui/project-insert/Step4.tsx +++ b/src/features/projects/ui/project-insert/Step4.tsx @@ -1,20 +1,72 @@ -import { Box, styled } from "@mui/material"; +import { Box, styled, useMediaQuery, useTheme } from "@mui/material"; import type { JSX } from "react"; -const Step4 = (): JSX.Element => { - return Step4; +import type { Step4Type } from "@features/projects/hook/useProjectInsertForm"; + +import ProjectPreferentialCard from "@entities/projects/ui/project-insert/ProjectPreferentialCard"; +import ProjectRequirementsCard from "@entities/projects/ui/project-insert/ProjectRequirementsCard"; +import ProjectWorkflowCard from "@entities/projects/ui/project-insert/ProjectWorkflowCard"; + +import { Workflow } from "@shared/types/project"; + +interface Step4Props { + form: Step4Type; + onChangeForm: ( + field: keyof Step4Type, + value: Step4Type[keyof Step4Type] + ) => void; +} + +const Step4 = ({ form, onChangeForm }: Step4Props): JSX.Element => { + const theme = useTheme(); + const isMdDown = useMediaQuery(theme.breakpoints.down("md")); + + // 안전 장치: form이 없으면 로딩 상태 표시 + if (!form) { + return
Loading...
; + } + + return ( + + {/* 진행 방식 - 전체 너비 */} + onChangeForm("workflow", value)} + large + style={{ gridColumn: isMdDown ? "span 1" : "1 / -1" }} + /> + + {/* 지원 요구사항 - 전체 너비 */} + onChangeForm("requirements", value)} + large + style={{ gridColumn: isMdDown ? "span 1" : "1 / -1" }} + /> + + {/* 우대사항 - 전체 너비 */} + + onChangeForm("preferentialTreatment", value) + } + large + style={{ gridColumn: isMdDown ? "span 1" : "1 / -1" }} + /> + + ); }; export default Step4; export const StepBox = styled(Box)(({ theme }) => ({ display: "grid", - gridTemplateColumns: "1fr", // 기본값(xs) - gap: theme.spacing(2), // 기본값(sm 이하) + gridTemplateColumns: "1fr", + gap: theme.spacing(2), marginBottom: 0, [theme.breakpoints.up("md")]: { gridTemplateColumns: "1fr 1fr", - gap: theme.spacing(4), + gap: theme.spacing(3), }, })); diff --git a/src/pages/project-insert/ui/ProjectInsertPage.tsx b/src/pages/project-insert/ui/ProjectInsertPage.tsx index dd753ea..d357fe0 100644 --- a/src/pages/project-insert/ui/ProjectInsertPage.tsx +++ b/src/pages/project-insert/ui/ProjectInsertPage.tsx @@ -32,8 +32,12 @@ const ProjectInsertPage = (): JSX.Element => { {page.currentStep === 2 && ( )} - {page.currentStep === 3 && } - {page.currentStep === 4 && } + {page.currentStep === 3 && ( + + )} + {page.currentStep === 4 && ( + + )} {/* 네비게이션 버튼 */}