Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const shopRoutes: RouteObject[] = [
{
path: ROUTES.SHOP.REGISTER,
Component: ShopRegisterPage,
handle: { hideFooter: true },
},
{
path: ROUTES.SHOP.EDIT,
Expand Down
12 changes: 6 additions & 6 deletions src/assets/icon/camera.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ const textSizeClassMap: Record<ButtonTextSize, string> = {
};

const variantClassMap: Record<ButtonVariant, string> = {
primary: "bg-[#EA3C12] text-white hover:bg-[#ca3f2a] active:bg-[#aa3523]",
primary:
"bg-[#EA3C12] text-white hover:bg-[#ca3f2a] active:bg-[#aa3523] cursor-pointer",
white:
"bg-white text-[#EA3C12] border border-[#EA3C12] hover:bg-[#fff5f3] active:bg-[#ffe5e0]",
"bg-white text-[#EA3C12] border border-[#EA3C12] hover:bg-[#fff5f3] active:bg-[#ffe5e0] cursor-pointer",
};

const disabledClass = "bg-gray-40 text-white cursor-not-allowed";
Expand Down
12 changes: 10 additions & 2 deletions src/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Outlet } from "react-router-dom";
import { Outlet, useMatches } from "react-router-dom";

import Footer from "./Footer";
import Header from "./Header";

interface MainLayoutProps {
isLoggedIn?: boolean;
userNavLabel?: "내 가게" | "내 프로필";
Expand All @@ -10,13 +11,20 @@ interface MainLayoutProps {
onToggleAlarm?: () => void;
}

interface RouteHandle {
hideFooter?: boolean;
}

export default function MainLayout({
isLoggedIn = false,
userNavLabel,
hasAlarm,
onLogout,
onToggleAlarm,
}: MainLayoutProps) {
const matches = useMatches() as Array<{ handle?: RouteHandle }>;
const hideFooter = matches.some((match) => match.handle?.hideFooter);

return (
<div className="w-full min-h-screen flex flex-col">
<Header
Expand All @@ -31,7 +39,7 @@ export default function MainLayout({
<Outlet />
</main>

<Footer />
{!hideFooter && <Footer />}
</div>
);
}
230 changes: 229 additions & 1 deletion src/pages/ShopRegisterPage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,231 @@
import { ChangeEvent, 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 { postShop } 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 { extractDigits, numberCommaFormatter } from "@/utils/number";

const CATEGORY_OPTIONS = [
{ label: "한식", value: "한식" },
{ label: "중식", value: "중식" },
{ label: "일식", value: "일식" },
{ label: "양식", value: "양식" },
{ label: "분식", value: "분식" },
{ label: "카페", value: "카페" },
{ label: "편의점", value: "편의점" },
{ label: "기타", value: "기타" },
];

export default function ShopRegisterPage() {
return <div>ShopRegisterPage</div>;
const navigate = useNavigate();
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [imageFile, setImageFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);

const [form, setForm] = useState({
name: "",
category: "" as ShopCategory,
address1: "" as SeoulDistrict,
address2: "",
originalHourlyPay: "",
description: "",
});

const handleChange = (
key: keyof typeof form,
value: string | SeoulDistrict | ShopCategory,
) => {
setForm((prev) => ({ ...prev, [key]: value }));
};

const handleImageUpload = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;

setImageFile(file);

const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") {
setImagePreview(reader.result);
}
};
reader.readAsDataURL(file);
};

const handleSubmit = async () => {
if (isSubmitting) return;

const requiredFields: Array<keyof typeof form> = [
"name",
"category",
"address1",
"address2",
"originalHourlyPay",
];

const missingField = requiredFields.find((key) => form[key].trim() === "");

if (missingField) {
alert("모든 필수 항목을 입력해 주세요.");
return;
}

const hourlyPay = Number(extractDigits(form.originalHourlyPay));
if (isNaN(hourlyPay) || hourlyPay <= 0) {
alert("유효한 시급을 입력해 주세요.");
return;
}

try {
setIsSubmitting(true);

let imageUrl = "";
if (imageFile) {
const presignedURL = await postImage(imageFile.name);
await putImage(presignedURL, imageFile);
imageUrl = getPublicURL(presignedURL);
}

const payload = {
name: form.name.trim(),
category: form.category,
address1: form.address1,
address2: form.address2.trim(),
originalHourlyPay: hourlyPay,
description: form.description.trim(),
imageUrl,
};

const res = await postShop(payload);
console.log("등록 성공:", res.data);
navigate("/shop");
} catch (err) {
console.error("등록 실패:", err);
alert("가게 등록 중 오류가 발생했습니다.");
} finally {
setIsSubmitting(false);
}
};

return (
<div className="w-full max-w-[964px] mx-auto px-4 py-12">
<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 sm:grid-cols-2 grid-cols-1 gap-5 mb-6">
<TextField.Input
label="가게 이름*"
placeholder="입력"
fullWidth
value={form.name}
onChange={(e) => handleChange("name", e.target.value)}
/>
<Select
label="분류*"
placeholder="선택"
fullWidth
options={CATEGORY_OPTIONS}
value={form.category}
onValueChange={(value) => handleChange("category", value)}
/>
<Select
label="주소*"
placeholder="선택"
fullWidth
options={SeoulDistricts.map((d) => ({ label: d, value: d }))}
value={form.address1}
onValueChange={(value) => handleChange("address1", value)}
/>
<TextField.Input
label="상세 주소*"
placeholder="입력"
fullWidth
value={form.address2}
onChange={(e) => handleChange("address2", e.target.value)}
/>
<TextField.Input
label="기본 시급*"
placeholder="입력"
fullWidth
value={form.originalHourlyPay}
onChange={(e) => {
const rawValue = e.target.value;
const digitsOnly = extractDigits(rawValue);
const formatted = digitsOnly
? numberCommaFormatter(Number(digitsOnly))
: "";
handleChange("originalHourlyPay", formatted);
}}
postfix={<span className="text-black mr-2"></span>}
/>
</div>
<div className="mb-6">
<label className="block mb-2">가게 이미지</label>
<div
className="sm:w-[483px] sm:h-[276px] w-full h-[200px] rounded-lg bg-gray-10 border border-gray-30 flex justify-center items-center overflow-hidden cursor-pointer"
onClick={() => fileInputRef.current?.click()}
>
{imagePreview ? (
<img
src={imagePreview}
alt="preview"
className="object-cover w-full h-full"
/>
) : (
<div className="flex flex-col items-center text-gray-40">
<Camera className="w-8 h-8 mb-2.75" />
<span>이미지 추가하기</span>
</div>
)}
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleImageUpload}
/>
</div>
<div className="mb-10">
<TextField.TextArea
label="가게 설명"
placeholder="입력"
fullWidth
rows={4}
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}
onClick={handleSubmit}
>
등록하기
</Button>
</div>
</div>
);
}
8 changes: 8 additions & 0 deletions src/utils/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@
export const numberCommaFormatter = Intl.NumberFormat("ko-kr", {
compactDisplay: "long",
}).format;

/**
* 콤마가 포함된 숫자를 일반 숫자형으로 반환합니다.
* @param value 콤마가 포함된 문자형 숫자
* @returns 숫자형
* @example extractDigits(20,000) -> 20000
*/
export const extractDigits = (value: string) => value.replace(/[^\d]/g, "");