Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"clsx": "^2.1.1",
"postcss": "^8.5.3",
"react": "^19.0.0",
"react-datepicker": "^8.3.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.1",
"tailwind-merge": "^3.2.0",
Expand Down
188 changes: 187 additions & 1 deletion src/pages/NoticeRegisterPage.tsx
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description 속성은 requiredFields에 포함되지 않아서 옵셔널로 주어도 좋을 것 같습니다.!

Copy link
Collaborator Author

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() ?? ""와 같은 처리가 필요해서 불필요하게 복잡해진다고 생각합니당

입력은 선택이지만, 항상 빈 문자열("")을 기본값으로 가진다는 가정하에 사용하는 느낌입니다!

};

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> = [
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 () => { ... };
  ...
}

이 변수는 컴포넌트 외부로 위치시켜도 좋을 것 같아요 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 페이지들에서처럼 missingField를 다루는 조건문 위에서 requiredFields를 다루는 게 코드를 보기에 이해하기 좋을 거 같다고 생각했는데, 혹시 공고 등록 페이지에서만 특히 requiredFields를 컴포넌트 밖으로 이동하는 게 좋다고 느낀 이유가 있으실까요..?!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 다른 페이지에서도 동일한 의견이긴 합니다!
사용되는 곳과 가까이 변수 두기 <-> 메서드 실행시마다 배열 재생성(및 해당 페이지에서 필수 필드는 이거구나라는 명시적인 표현?)
중 어떤 것을 더 챙겨보냐의 차이일 것 같아요! 다른 페이지도 저는 동일한 의견이긴 합니다 하핳

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description이 옵셔널이므로 requiredFields에 포함이 안되는 건 맞지만, 만약 description이 입력되었을 때 "최대 길이 500자" 같은 유효성 검사 로직을 고려해봐도 좋을 것 같습니다 ~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 TextArea 자체에 maxLength={500}을 두는 방식으로 구현해보았습니다. 추후에 UX 개선 과정에서 라벨 자체에 '공고 설명 (최대 500자)' 이런 식으로 설정하는 방식을 논의해보면 좋을 것 같습니다!!

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>
);
}