diff --git a/src/api/imageApi.ts b/src/api/imageApi.ts index 16ae08d4..db7fca01 100644 --- a/src/api/imageApi.ts +++ b/src/api/imageApi.ts @@ -1,11 +1,7 @@ import api from './api'; -import { AxiosError } from 'axios'; +import axios, { AxiosError } from 'axios'; import type { LinkInfo } from './shopApi'; -interface ErrorMessage { - message: string; -} - interface ImageResponse { item: { url: string; // ✅ 쿼리 스트링 포함된 presigned S3 PUT URL @@ -21,9 +17,9 @@ export const getPresignedUrl = async (filename: string): Promise => { }); return response.data.item.url; } catch (error) { - const axiosError = error as AxiosError; // 에러 타입 명시 + const axiosError = error as AxiosError; // 에러 타입 명시 if (axiosError.response) { - throw new Error(axiosError.response.data.message); + throw new Error('URL 생성에 실패했습니다.'); } else { throw new Error('서버에 연결할 수 없습니다. 인터넷 연결을 확인해주세요.'); } @@ -36,15 +32,15 @@ export const uploadImageToS3 = async ( file: File, ): Promise => { try { - await api.put(uploadUrl, file, { + await axios.put(uploadUrl, file, { headers: { 'Content-Type': file.type, }, }); } catch (error) { - const axiosError = error as AxiosError; // 에러 타입 명시 + const axiosError = error as AxiosError; // 에러 타입 명시 if (axiosError.response) { - throw new Error(axiosError.response.data.message); + throw new Error('이미지 업로드에 실패했습니다.'); } else { throw new Error('서버에 연결할 수 없습니다. 인터넷 연결을 확인해주세요.'); } diff --git a/src/pages/store/StoreEdit.tsx b/src/pages/store/StoreEdit.tsx index 9c38afbf..b1c77cb4 100644 --- a/src/pages/store/StoreEdit.tsx +++ b/src/pages/store/StoreEdit.tsx @@ -1,3 +1,291 @@ +import { useNavigate } from 'react-router-dom'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import Close from '@/assets/icons/close.svg'; +import { ADDRESS_OPTIONS, CATEGORY_OPTIONS } from '@/constants/dropdownOptions'; +import { getShop, postShop, putShop, type ShopRequest } from '@/api/shopApi'; +import { getPresignedUrl, uploadImageToS3 } from '@/api/imageApi'; +import Input from '@/components/common/Input'; +import Dropdown from '@/components/common/Dropdown'; +import ImageInput from '@/components/common/ImageInput'; +import Button from '@/components/common/Button'; +import Modal from '@/components/common/Modal'; +import { AuthContext } from '@/context/AuthContext'; +import { getUser } from '@/api/userApi'; + +type Category = (typeof CATEGORY_OPTIONS)[number]; +type Address1 = (typeof ADDRESS_OPTIONS)[number]; + +interface StoreEditForm extends Omit { + category: Category | null; + address1: Address1 | null; +} + +type ModalType = 'success' | 'warning' | 'auth'; + export default function StoreEdit() { - return
내 가게 정보 등록/편집
; + const navigate = useNavigate(); + const { isLoggedIn } = useContext(AuthContext); + + const [edit, setEdit] = useState({ + name: '', + category: null, + address1: null, + address2: '', + description: '', + originalHourlyPay: 0, + imageUrl: '', + }); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalContent, setModalContent] = useState(''); + const [modalType, setModalType] = useState('success'); + + const [shopId, setShopId] = useState(null); + + // 로그아웃 처리 및 등록 수정 모드 + useEffect(() => { + const fetchInitialData = async () => { + const userId = localStorage.getItem('userId'); + + if (!userId) { + setModalType('auth'); + setModalContent('로그인이 필요합니다.'); + setIsModalOpen(true); + return; + } + + try { + const user = await getUser(userId); + const id = user.item.shop?.item.id ?? null; + setShopId(id); + + if (!id) return; // 등록 모드 + + const shopInfo = await getShop(id); + setEdit(shopInfo.item); + } catch (error) { + setModalType('warning'); + setModalContent('가게 정보를 불러오는 데 실패했습니다.'); + setIsModalOpen(true); + } + }; + + fetchInitialData(); + }, [isLoggedIn]); + + // x 버튼 눌렀을 때 + const handleClose = () => { + navigate('/owner/store'); + }; + + // 공통 문자열 핸들러 + const handleChange = useCallback( + (key: keyof StoreEditForm) => + (e: React.ChangeEvent) => { + setEdit((prev) => ({ ...prev, [key]: e.target.value })); + }, + [], + ); + + // 숫자 전용 핸들러 + const handleNumberChange = useCallback( + (key: keyof StoreEditForm) => (e: React.ChangeEvent) => { + const raw = e.target.value.replace(/[^0-9]/g, ''); + setEdit((prev) => ({ ...prev, [key]: Number(raw) })); + }, + [], + ); + + // 이미지 핸들러 + const handleImageChange = async (file: File) => { + try { + const presignedUrl = await getPresignedUrl(file.name); + const fileUrl = presignedUrl.split('?')[0]; // S3 저장용 URL(쿼리 제거) + + await uploadImageToS3(presignedUrl, file); + + setEdit((prev) => ({ ...prev, imageUrl: fileUrl })); + } catch (error) { + alert((error as Error).message); + } + }; + + // 등록 버튼 처리 + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // 필수 입력 값 + const requiredFields = [ + { key: 'name', label: '가게 이름' }, + { key: 'category', label: '분류' }, + { key: 'address1', label: '주소' }, + { key: 'address2', label: '상세 주소' }, + { key: 'originalHourlyPay', label: '기본 시급' }, + { key: 'imageUrl', label: '가게 이미지' }, + ]; + + // 입력 안할 시 모달로 알려줌 + for (const { key, label } of requiredFields) { + const value = edit[key as keyof StoreEditForm]; + + const isEmpty = typeof value === 'string' ? value.trim() === '' : !value; + + if (isEmpty) { + setModalType('warning'); + setModalContent(`${label} 내용을 추가해 주세요.`); + setIsModalOpen(true); + return; + } + } + + try { + const requestBody: ShopRequest = { + ...edit, + category: edit.category as ShopRequest['category'], + address1: edit.address1 as ShopRequest['address1'], + }; + + // 등록, 수정을 구분 + if (shopId) { + await putShop(shopId, requestBody); + setModalType('success'); + setModalContent('수정이 완료되었습니다.'); + } else { + await postShop(requestBody); + setModalType('success'); + setModalContent('등록이 완료되었습니다.'); + } + + setIsModalOpen(true); + } catch (error) { + setModalType('warning'); + setModalContent((error as Error).message); + setIsModalOpen(true); + } + }; + + // 모달 버튼 기능 + const handleModalClose = () => { + setIsModalOpen(false); + + switch (modalType) { + case 'success': + navigate('/owner/store'); + break; + case 'auth': + navigate('/login'); + break; + default: + break; + } + }; + + return ( +
+
+
+

+ 가게 정보 +

+ +
+
+
+
+ +
+ + + options={CATEGORY_OPTIONS} + selected={edit.category} + setSelect={(value) => + setEdit((prev) => ({ + ...prev, + category: value as Category, + })) + } + variant="form" + /> +
+
+ + + options={ADDRESS_OPTIONS} + selected={edit.address1} + setSelect={(value) => + setEdit((prev) => ({ + ...prev, + address1: value as Address1, + })) + } + variant="form" + /> +
+ + +
+ +
+ +
+ +
+ +