diff --git a/src/app/Host_post/SpaceCreateForm.tsx b/src/app/Host_post/SpaceCreateForm.tsx new file mode 100644 index 0000000..b8d523b --- /dev/null +++ b/src/app/Host_post/SpaceCreateForm.tsx @@ -0,0 +1,470 @@ +"use client"; + +import { type FormEvent, useState } from "react"; +import { getAccessToken } from "@/lib/current-user"; + +const infoFields = [ + { + id: "category", + label: "카테고리", + type: "select" as const, + options: ["팝업 스토어", "전시", "쇼룸", "스튜디오"], + }, + { + id: "region", + label: "지역 / 주소", + type: "input" as const, + placeholder: "예) 서울 성수동", + }, + { + id: "capacity", + label: "수용 인원", + type: "input" as const, + placeholder: "예) 최대 100명", + }, + { + id: "schedule", + label: "운영 가능 일정", + type: "select" as const, + options: ["평일 / 주말 모두 가능", "평일만 가능", "주말만 가능"], + }, + { + id: "price", + label: "희망 대관 단가", + type: "input" as const, + placeholder: "예) 120000", + }, + { + id: "contact", + label: "연락 방식", + type: "select" as const, + options: ["이메일 + 플랫폼 메시지", "이메일", "플랫폼 메시지"], + }, + { + id: "lat", + label: "위도", + type: "input" as const, + placeholder: "예) 37.5443", + }, + { + id: "lng", + label: "경도", + type: "input" as const, + placeholder: "예) 127.0557", + }, +] as const; + +const initialForm = { + title: "예) 성수 브랜드 팝업 전용 쇼룸", + description: + "예) 성수 메인 동선에 위치한 이 공간은 브랜드 팝업, 쇼룸, 전시에 잘 어울립니다.\n전면 유리와 높은 층고 덕분에 낮에는 자연광이 풍부하고, 저녁에는 조명 연출이 깔끔하게 살아납니다.\n\n가구 배치가 유연해서 행사 성격에 맞게 동선을 구성하기 좋고, 방문객이 사진 찍기 좋은 포인트도 충분합니다.", + category: "팝업 스토어", + region: "서울 성수동", + capacity: "100명", + schedule: "평일 / 주말 모두 가능", + price: "120000", + contact: "이메일 + 플랫폼 메시지", + lat: "37.5443", + lng: "127.0557", +}; + +type FormState = typeof initialForm; +type StatusType = "idle" | "saving" | "success" | "error"; + +const DRAFT_KEY = "momentlit.space-create.draft"; + +function loadDraft() { + if (typeof window === "undefined") { + return null; + } + + const savedDraft = window.sessionStorage.getItem(DRAFT_KEY); + + if (!savedDraft) { + return null; + } + + try { + const parsedDraft = JSON.parse(savedDraft) as { + form?: Partial; + }; + + return parsedDraft.form ?? null; + } catch { + window.sessionStorage.removeItem(DRAFT_KEY); + return null; + } +} + +function getResponseMessage(payload: unknown) { + if (!payload || typeof payload !== "object") { + return null; + } + + const message = (payload as { message?: unknown }).message; + + return typeof message === "string" ? message : null; +} + +function getResponseSpaceId(payload: unknown) { + if (!payload || typeof payload !== "object") { + return null; + } + + const data = (payload as { data?: unknown }).data; + + if (!data || typeof data !== "object") { + return null; + } + + const spaceId = (data as { space_id?: unknown }).space_id; + + return typeof spaceId === "number" ? spaceId : null; +} + +function fileToDataUrl(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = () => { + if (typeof reader.result === "string") { + resolve(reader.result); + return; + } + + reject(new Error("이미지를 변환하지 못했습니다.")); + }; + + reader.onerror = () => { + reject(new Error("이미지를 읽는 중 오류가 발생했습니다.")); + }; + + reader.readAsDataURL(file); + }); +} + +function FieldControl({ + field, + onChange, + value, +}: { + field: (typeof infoFields)[number]; + onChange: (name: keyof FormState, value: string) => void; + value: string; +}) { + const commonClassName = + "h-[58px] w-full rounded-[16px] border border-[#C8E4E6] bg-white px-[18px] text-[15px] font-medium text-[#222831] outline-none transition placeholder:text-[#A1AAB8] focus:border-[#00ADB5] focus:ring-2 focus:ring-[#00ADB5]/10"; + + if (field.type === "select") { + return ( + + ); + } + + return ( + + ); +} + +export function SpaceCreateForm({ + imageFiles, + selectedDates, +}: { + imageFiles: (File | null)[]; + selectedDates: string[]; +}) { + const [form, setForm] = useState(() => ({ + ...initialForm, + ...loadDraft(), + })); + const [statusMessage, setStatusMessage] = useState(() => + loadDraft() ? "임시 저장된 내용을 불러왔습니다." : "", + ); + const [statusType, setStatusType] = useState("idle"); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleFieldChange = (name: keyof FormState, value: string) => { + setForm((current) => ({ ...current, [name]: value })); + }; + + const handleDraftSave = () => { + window.sessionStorage.setItem( + DRAFT_KEY, + JSON.stringify({ + form, + }), + ); + setStatusType("success"); + setStatusMessage("임시 저장했습니다."); + }; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setStatusType("idle"); + setStatusMessage(""); + + const accessToken = getAccessToken(); + + if (!accessToken) { + setStatusType("error"); + setStatusMessage("로그인 후 다시 시도해주세요."); + return; + } + + const pricePerHour = Number(form.price.replace(/[^\d]/g, "")); + const lat = Number(form.lat); + const lng = Number(form.lng); + const selectedFiles = imageFiles.filter((file): file is File => Boolean(file)); + + if (selectedFiles.length === 0) { + setStatusType("error"); + setStatusMessage("대표 사진을 선택해주세요."); + return; + } + + if (Number.isNaN(pricePerHour) || pricePerHour <= 0) { + setStatusType("error"); + setStatusMessage("희망 대관 단가를 숫자로 입력해주세요."); + return; + } + + if (Number.isNaN(lat) || Number.isNaN(lng)) { + setStatusType("error"); + setStatusMessage("위도와 경도를 숫자로 입력해주세요."); + return; + } + + setIsSubmitting(true); + setStatusType("saving"); + + try { + const imageDataUrls = await Promise.all(selectedFiles.map(fileToDataUrl)); + + const response = await fetch("/api/spaces", { + body: JSON.stringify({ + name: form.title.trim(), + description: form.description.trim() || null, + address: form.region.trim(), + thumbnail_url: imageDataUrls[0], + image_urls: imageDataUrls, + price_per_hour: pricePerHour, + category: form.category, + lat, + lng, + }), + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + method: "POST", + }); + + const responseText = await response.text(); + let payload: unknown = null; + + if (responseText) { + try { + payload = JSON.parse(responseText) as unknown; + } catch { + payload = { message: responseText }; + } + } + + if (!response.ok) { + const message = getResponseMessage(payload) ?? "등록 요청에 실패했습니다."; + + throw new Error(message); + } + + const spaceId = getResponseSpaceId(payload); + + window.sessionStorage.removeItem(DRAFT_KEY); + setStatusType("success"); + setStatusMessage( + spaceId + ? `공간 등록 요청이 완료되었습니다. 공간 ID ${spaceId}` + : "공간 등록 요청이 완료되었습니다.", + ); + } catch (error) { + setStatusType("error"); + setStatusMessage( + error instanceof Error ? error.message : "등록 요청에 실패했습니다.", + ); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+

제목

+
+ + 선택 일정 {selectedDates.length}일 + + + 이미지 {imageFiles.filter((file) => file).length}장 + +
+
+ +
+ +
+

공간소개

+