diff --git a/src/assets/icon/camera.svg b/src/assets/icon/camera.svg index 2daf266..9437a87 100644 --- a/src/assets/icon/camera.svg +++ b/src/assets/icon/camera.svg @@ -1,8 +1,8 @@ - - - + + + diff --git a/src/constants/shopCategory.ts b/src/constants/shopCategory.ts new file mode 100644 index 0000000..6db6dcc --- /dev/null +++ b/src/constants/shopCategory.ts @@ -0,0 +1,10 @@ +export const CATEGORY_OPTIONS = [ + "한식", + "중식", + "일식", + "양식", + "분식", + "카페", + "편의점", + "기타", +].map((category) => ({ label: category, value: category })); diff --git a/src/pages/ShopEditPage.tsx b/src/pages/ShopEditPage.tsx index 0122373..1e7c1e4 100644 --- a/src/pages/ShopEditPage.tsx +++ b/src/pages/ShopEditPage.tsx @@ -1,3 +1,267 @@ -export default function ShopEditPage() { - return
ShopEditPage
; +import { ChangeEvent, useEffect, useRef, useState } from "react"; + +import { useNavigate } from "react-router-dom"; + +import { SeoulDistrict, SeoulDistricts, ShopCategory } from "../types"; + +import { + getPublicURL, + postImage, + putImage, +} from "@/apis/services/imageService"; +import { getShop, putShop } from "@/apis/services/shopService"; +import { Camera, Close } from "@/assets/icon"; +import Button from "@/components/Button"; +import Select from "@/components/Select"; +import TextField from "@/components/TextField"; +import { ROUTES } from "@/constants/router"; +import { CATEGORY_OPTIONS } from "@/constants/shopCategory"; +import { useUserStore } from "@/hooks/useUserStore"; +import { extractDigits, numberCommaFormatter } from "@/utils/number"; + +type FormType = { + name: string; + category: ShopCategory; + address1: SeoulDistrict; + address2: string; + originalHourlyPay: string; + description: string; +}; + +const FIELD_LABELS: Record = { + name: "가게 이름", + category: "분류", + address1: "주소", + address2: "상세 주소", + originalHourlyPay: "기본 시급", + description: "가게 설명", +}; + +export default function ShopRegisterPage() { + const navigate = useNavigate(); + const { user } = useUserStore(); + const shopId = user?.shopId; + const fileInputRef = useRef(null); + const [imagePreview, setImagePreview] = useState(null); + const [imageFile, setImageFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [form, setForm] = useState({ + name: "", + category: "" as ShopCategory, + address1: "" as SeoulDistrict, + address2: "", + originalHourlyPay: "", + description: "", + }); + + useEffect(() => { + async function fetchShop() { + if (!shopId) return; + const res = await getShop(shopId); + const { + name, + category, + address1, + address2, + originalHourlyPay, + description, + imageUrl, + } = res.data.item; + setForm({ + name, + category, + address1, + address2, + originalHourlyPay: numberCommaFormatter(originalHourlyPay), + description, + }); + if (imageUrl) setImagePreview(imageUrl); + } + fetchShop(); + }, [shopId]); + + const handleChange = ( + key: keyof FormType, + value: string | SeoulDistrict | ShopCategory, + ) => { + setForm((prev) => ({ ...prev, [key]: value })); + }; + + const handleImageUpload = (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setImageFile(file); + + if (imagePreview) URL.revokeObjectURL(imagePreview); + const blobUrl = URL.createObjectURL(file); + setImagePreview(blobUrl); + }; + + const handleSubmit = async () => { + if (isSubmitting || !shopId) return; + + const requiredFields: Array = [ + "name", + "category", + "address1", + "address2", + "originalHourlyPay", + ]; + + const missingField = requiredFields.find((key) => form[key].trim() === ""); + + if (missingField) { + alert(`${FIELD_LABELS[missingField]}을(를) 입력해 주세요.`); + return; + } + + const hourlyPay = Number(extractDigits(form.originalHourlyPay)); + if (isNaN(hourlyPay) || hourlyPay <= 0) { + alert("유효한 시급을 입력해 주세요."); + return; + } + + setIsSubmitting(true); + let imageUrl = imagePreview || ""; + + const payload = { + name: form.name.trim(), + category: form.category, + address1: form.address1, + address2: form.address2.trim(), + originalHourlyPay: hourlyPay, + description: form.description.trim(), + imageUrl, + }; + + try { + if (imageFile) { + const presignedURL = await postImage(imageFile.name); + await putImage(presignedURL, imageFile); + imageUrl = getPublicURL(presignedURL); + payload.imageUrl = imageUrl; + } + await putShop(shopId, payload); + navigate(ROUTES.SHOP.ROOT); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
{ + e.preventDefault(); + handleSubmit(); + }} + > +
+

+ 가게 정보 +

+ +
+ +
+ handleChange("name", e.target.value)} + /> + ({ label: d, value: d }))} + value={form.address1} + onValueChange={(value) => handleChange("address1", value)} + /> + handleChange("address2", e.target.value)} + /> + { + const rawValue = e.target.value; + const digitsOnly = extractDigits(rawValue); + const formatted = digitsOnly + ? numberCommaFormatter(Number(digitsOnly)) + : ""; + handleChange("originalHourlyPay", formatted); + }} + postfix={} + /> +
+
+ +
fileInputRef.current?.click()} + > + {imagePreview && ( + preview + )} +
+
+ + 이미지 변경하기 +
+
+ +
+
+ handleChange("description", e.target.value)} + /> +
+
+ +
+ + ); } diff --git a/src/pages/ShopRegisterPage.tsx b/src/pages/ShopRegisterPage.tsx index c103bc1..fc92382 100644 --- a/src/pages/ShopRegisterPage.tsx +++ b/src/pages/ShopRegisterPage.tsx @@ -14,19 +14,10 @@ import { Camera, Close } from "@/assets/icon"; import Button from "@/components/Button"; import Select from "@/components/Select"; import TextField from "@/components/TextField"; +import { ROUTES } from "@/constants/router"; +import { CATEGORY_OPTIONS } from "@/constants/shopCategory"; import { extractDigits, numberCommaFormatter } from "@/utils/number"; -const CATEGORY_OPTIONS = [ - { label: "한식", value: "한식" }, - { label: "중식", value: "중식" }, - { label: "일식", value: "일식" }, - { label: "양식", value: "양식" }, - { label: "분식", value: "분식" }, - { label: "카페", value: "카페" }, - { label: "편의점", value: "편의점" }, - { label: "기타", value: "기타" }, -]; - type FormType = { name: string; category: ShopCategory; @@ -62,7 +53,7 @@ export default function ShopRegisterPage() { }); const handleChange = ( - key: keyof typeof form, + key: keyof FormType, value: string | SeoulDistrict | ShopCategory, ) => { setForm((prev) => ({ ...prev, [key]: value })); @@ -74,19 +65,17 @@ export default function ShopRegisterPage() { setImageFile(file); - const reader = new FileReader(); - reader.onload = () => { - if (typeof reader.result === "string") { - setImagePreview(reader.result); - } - }; - reader.readAsDataURL(file); + if (imagePreview) { + URL.revokeObjectURL(imagePreview); + } + const blobUrl = URL.createObjectURL(file); + setImagePreview(blobUrl); }; const handleSubmit = async () => { if (isSubmitting) return; - const requiredFields: Array = [ + const requiredFields: Array = [ "name", "category", "address1", @@ -107,28 +96,28 @@ export default function ShopRegisterPage() { return; } - try { - setIsSubmitting(true); + setIsSubmitting(true); + + let imageUrl = ""; + const payload = { + name: form.name.trim(), + category: form.category, + address1: form.address1, + address2: form.address2.trim(), + originalHourlyPay: hourlyPay, + description: form.description.trim(), + imageUrl, + }; - let imageUrl = ""; + try { if (imageFile) { const presignedURL = await postImage(imageFile.name); await putImage(presignedURL, imageFile); imageUrl = getPublicURL(presignedURL); + payload.imageUrl = imageUrl; } - - const payload = { - name: form.name.trim(), - category: form.category, - address1: form.address1, - address2: form.address2.trim(), - originalHourlyPay: hourlyPay, - description: form.description.trim(), - imageUrl, - }; - await postShop(payload); - navigate("/shop"); + navigate(ROUTES.SHOP.ROOT); } finally { setIsSubmitting(false); }