-
Notifications
You must be signed in to change notification settings - Fork 4
[feat] 공고 등록 페이지 구현 #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,189 @@ | ||
| import { useState } from "react"; | ||
|
|
||
| import DatePicker from "react-datepicker"; | ||
| import { useNavigate } from "react-router-dom"; | ||
|
|
||
| import "react-datepicker/dist/react-datepicker.css"; | ||
|
|
||
| import { postNotice } from "@/apis/services/noticeService"; | ||
| import { Close } from "@/assets/icon"; | ||
| import Button from "@/components/Button"; | ||
| import TextField from "@/components/TextField"; | ||
| import { ROUTES } from "@/constants/router"; | ||
| import { useUserStore } from "@/hooks/useUserStore"; | ||
| import { extractDigits, numberCommaFormatter } from "@/utils/number"; | ||
|
|
||
| type FormType = { | ||
| hourlyPay: string; | ||
| startsAt: Date | null; | ||
| workhour: string; | ||
| description: string; | ||
| }; | ||
|
|
||
| const FIELD_LABELS: Record<keyof FormType, string> = { | ||
| hourlyPay: "시급", | ||
| startsAt: "시작 일시", | ||
| workhour: "업무 시간", | ||
| description: "공고 설명", | ||
| }; | ||
|
|
||
| export default function NoticeRegisterPage() { | ||
| return <div>NoticeRegisterPage</div>; | ||
| const navigate = useNavigate(); | ||
| const { user } = useUserStore(); | ||
| const [isSubmitting, setIsSubmitting] = useState(false); | ||
|
|
||
| const [form, setForm] = useState<FormType>({ | ||
| hourlyPay: "", | ||
| startsAt: null, | ||
| workhour: "", | ||
| description: "", | ||
| }); | ||
|
|
||
| const handleChange = (key: keyof FormType, value: string | null | Date) => { | ||
| setForm((prev) => ({ ...prev, [key]: value })); | ||
| }; | ||
|
|
||
| const handleSubmit = async () => { | ||
| if (!user?.id) { | ||
| alert("로그인 정보가 없습니다."); | ||
| return; | ||
| } | ||
|
|
||
| if (!user?.shopId) { | ||
| alert("가게 정보가 없습니다."); | ||
| return; | ||
| } | ||
|
|
||
| if (isSubmitting) return; | ||
|
|
||
| const requiredFields: Array<keyof FormType> = [ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // 컴포넌트 밖으로 이동
const requiredFields: Array<keyof FormType> = [
"hourlyPay",
"startsAt",
"workhour",
];
export default function NoticeRegisterPage() {
...
const handleSubmit = async () => { ... };
...
}이 변수는 컴포넌트 외부로 위치시켜도 좋을 것 같아요 👍
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다른 페이지들에서처럼
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 앗 다른 페이지에서도 동일한 의견이긴 합니다!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 매 렌더링 시에 배열이 재생성되긴 하지만 재생 비용이 미미하고 문맥 가독성을 챙기는 게 우선이라구 생각합니다..! 일단 그대로 진행해보겠습니다! 꼭 필요하다고 생각하신다면 팀회의 때 논의해본 후에 버그 픽스 과정 중에 바꿔도 될 것 같아요 🤓 |
||
| "hourlyPay", | ||
| "startsAt", | ||
| "workhour", | ||
| ]; | ||
|
|
||
| const missingField = requiredFields.find((key) => { | ||
| const value = form[key]; | ||
| return ( | ||
| value === null || (typeof value === "string" && value.trim() === "") | ||
| ); | ||
| }); | ||
|
|
||
| if (missingField) { | ||
| alert(`${FIELD_LABELS[missingField]}을(를) 입력해 주세요.`); | ||
| return; | ||
| } | ||
|
|
||
| const hourlyPay = Number(extractDigits(form.hourlyPay)); | ||
| if (isNaN(hourlyPay) || hourlyPay <= 0) { | ||
| alert("유효한 시급을 입력해 주세요."); | ||
| return; | ||
| } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. description이 옵셔널이므로 requiredFields에 포함이 안되는 건 맞지만, 만약 description이 입력되었을 때 "최대 길이 500자" 같은 유효성 검사 로직을 고려해봐도 좋을 것 같습니다 ~
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 우선 TextArea 자체에 |
||
| const workhour = Number(form.workhour); | ||
| if (isNaN(workhour) || workhour <= 0) { | ||
| alert("유효한 업무 시간을 입력해 주세요."); | ||
| return; | ||
| } | ||
|
|
||
| setIsSubmitting(true); | ||
|
|
||
| const payload = { | ||
| hourlyPay: hourlyPay, | ||
| startsAt: form.startsAt!.toISOString(), | ||
| workhour: workhour, | ||
| description: form.description.trim(), | ||
| }; | ||
|
|
||
| try { | ||
| await postNotice(user.shopId, payload); | ||
| navigate(ROUTES.SHOP.ROOT); | ||
| } finally { | ||
| setIsSubmitting(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <form | ||
| className="w-full max-w-[964px] mx-auto px-4 py-12" | ||
| onSubmit={(e) => { | ||
| e.preventDefault(); | ||
| handleSubmit(); | ||
| }} | ||
| > | ||
| <div className="flex justify-between items-center mb-8"> | ||
| <h2 className="sm:text-[1.75rem] text-[1.25rem] font-bold"> | ||
| 공고 등록 | ||
| </h2> | ||
| <button onClick={() => navigate("/shop")}> | ||
| <Close className="sm:w-8 sm:h-8 w-6 h-6 cursor-pointer" /> | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 mb-6"> | ||
| <TextField.Input | ||
| label="시급*" | ||
| placeholder="입력" | ||
| fullWidth | ||
| value={form.hourlyPay} | ||
| onChange={(e) => { | ||
| const rawValue = e.target.value; | ||
| const digitsOnly = extractDigits(rawValue); | ||
| const formatted = digitsOnly | ||
| ? numberCommaFormatter(Number(digitsOnly)) | ||
| : ""; | ||
| handleChange("hourlyPay", formatted); | ||
| }} | ||
| postfix={<span className="text-black mr-2">원</span>} | ||
| /> | ||
| <div className="flex flex-col"> | ||
| <label className="inline-block mb-2 leading-[1.625rem]"> | ||
| 시작 일시* | ||
| </label> | ||
| <DatePicker | ||
| selected={form.startsAt} | ||
| onChange={(date) => handleChange("startsAt", date)} | ||
| showTimeSelect | ||
| timeFormat="HH:mm" | ||
| timeIntervals={10} | ||
| dateFormat="yyyy-MM-dd HH:mm" | ||
| placeholderText="선택" | ||
| className="w-full border border-gray-30 focus-within:border-blue-20 rounded-[0.375rem] py-4 px-5 text-[1rem]" | ||
| /> | ||
| </div> | ||
| <TextField.Input | ||
| label="업무 시간*" | ||
| placeholder="입력" | ||
| fullWidth | ||
| value={form.workhour} | ||
| onChange={(e) => handleChange("workhour", e.target.value)} | ||
| postfix={ | ||
| <span className="text-black mr-2 whitespace-nowrap">시간</span> | ||
| } | ||
| /> | ||
| </div> | ||
| <div className="mb-10"> | ||
| <TextField.TextArea | ||
| label="공고 설명" | ||
| placeholder="입력" | ||
| fullWidth | ||
| rows={4} | ||
| maxLength={500} | ||
| value={form.description} | ||
| onChange={(e) => handleChange("description", e.target.value)} | ||
| /> | ||
| </div> | ||
| <div className="text-center"> | ||
| <Button | ||
| variant="primary" | ||
| textSize="md" | ||
| className="sm:w-[350px] w-full px-34 py-3.5" | ||
| disabled={isSubmitting} | ||
| type="submit" | ||
| > | ||
| 등록하기 | ||
| </Button> | ||
| </div> | ||
| </form> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description 속성은 requiredFields에 포함되지 않아서 옵셔널로 주어도 좋을 것 같습니다.!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하단에서
description을 빈 문자열""로 초기화하여서 타입도string으로 맞췄습니다!만약 description을 옵셔널(
description?: string) 로 만들게 되면undefined일 가능성이 생기고,form.description.trim()과 같은 코드에서 에러가 발생하게 되더라구요. description이 옵셔널이 된다면 매번form.description?.trim() ?? ""와 같은 처리가 필요해서 불필요하게 복잡해진다고 생각합니당입력은 선택이지만, 항상 빈 문자열(
"")을 기본값으로 가진다는 가정하에 사용하는 느낌입니다!