Skip to content
Merged
25 changes: 10 additions & 15 deletions src/api/owner/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export interface StoreDetail {
isApproved: boolean;
rating?: number;
reviewCount?: number;
tableImages?: TableImage[];
}

export interface BusinessHour {
Expand Down Expand Up @@ -57,13 +56,13 @@ interface MyStoreResponse {
}

export interface TableImage {
tableId: number;
tableImageId: number;
tableImageUrl: string;
}

export interface TableImagesResponse {
storeId: number;
tableImageUrls: string[];
tableImages: TableImage[];
}

export function getStore(storeId: number | string) {
Expand Down Expand Up @@ -99,17 +98,13 @@ export const getMyStores = async (): Promise<MyStore[]> => {
export const getTableImages = async (
storeId: number | string,
): Promise<TableImage[]> => {
const res = await api.get<
ApiResponse<{ storeId: number; tableImageUrls: string[] }>
>(`/api/v1/stores/${storeId}/table-images`);
const res = await api.get<ApiResponse<TableImagesResponse>>(
`/api/v1/stores/${storeId}/table-images`,
);

if (!res.data.isSuccess) throw new Error(res.data.message);

// ⚠️ API 응답에 tableId가 포함되지 않아 삭제용 ID로 사용할 수 없음
return res.data.result.tableImageUrls.map((url) => ({
tableId: -1, // placeholder — 삭제 기능에 사용 금지
tableImageUrl: url,
}));
return res.data.result?.tableImages ?? [];
};

export const uploadTableImages = async (
Expand Down Expand Up @@ -139,12 +134,12 @@ export const uploadTableImages = async (

export const deleteTableImages = async (
storeId: number | string,
tableIds: number[],
): Promise<ApiResponse<{ tableId: number }>> => {
const response = await api.delete<ApiResponse<{ tableId: number }>>(
tableImageIds: number[],
): Promise<ApiResponse<{ tableImageId: number }>> => {
const response = await api.delete<ApiResponse<{ tableImageId: number }>>(
`/api/v1/stores/${storeId}/table-images`,
{
data: tableIds,
data: tableImageIds,
},
);
const data = response.data;
Expand Down
192 changes: 100 additions & 92 deletions src/components/owner/StoreSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { useEffect, useState } from "react";
import { Mail, Phone, MapPin, Clock, ChevronDown, X } from "lucide-react";
import { Mail, Phone, MapPin, Clock, ChevronDown } from "lucide-react";
import {
getStore,
updateStore,
updateBusinessHours,
uploadTableImages,
getTableImages,
type TableImage,
deleteTableImages,
uploadTableImages,
} from "@/api/owner/stores";

interface StoreSettingsProps {
Expand Down Expand Up @@ -36,13 +37,11 @@ const dayMapToApi: Record<string, any> = {
};

const StoreSettings: React.FC<StoreSettingsProps> = ({ storeId }) => {
const [storeName, setStoreName] = useState("맛있는 레스토랑");
const [description, setDescription] = useState(
"신선한 재료로 만드는 정성 가득한 음식을 제공합니다.",
);
const [phone, setPhone] = useState("02-1234-5678");
const [email, setEmail] = useState("store@example.com");
const [address, setAddress] = useState("서울특별시 강남구 테헤란로 123");
const [storeName, setStoreName] = useState("");
const [description, setDescription] = useState("");
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
const [address, setAddress] = useState("");

const [openTime, setOpenTime] = useState("11:00");
const [closeTime, setCloseTime] = useState("22:00");
Expand All @@ -53,41 +52,42 @@ const StoreSettings: React.FC<StoreSettingsProps> = ({ storeId }) => {
const [maxGuests, setMaxGuests] = useState<number | string>(20);

const [tableImages, setTableImages] = useState<TableImage[]>([]);
const [deletedIds, setDeletedIds] = useState<Set<number>>(new Set());
const [newFiles, setNewFiles] = useState<File[]>([]);
const [deletedIds, setDeletedIds] = useState<number[]>([]);

useEffect(() => {
if (!storeId) return;

getStore(storeId)
.then((res) => {
(async () => {
try {
const res = await getStore(storeId);
const store = res.data.result;

setStoreName(store.storeName);
setDescription(store.description ?? "");
setPhone(store.phone ?? "");
setAddress(store.address ?? "");

if (store.businessHours?.length) {
const open = store.businessHours.find((b) => !b.isClosed);
if (open?.openTime && open?.closeTime) {
setOpenTime(open.openTime);
setCloseTime(open.closeTime);
}

const closed = store.businessHours
.filter((b) => b.isClosed)
.map((b) => dayMapFromApi[b.day]);

setClosedDays(closed);
}
if (store.tableImages) {
setTableImages(store.tableImages);
}
})
.catch(() => {
alert("가게 정보를 불러오는 데 실패했습니다.");
});

const imgs = await getTableImages(storeId);
setTableImages(imgs);
setDeletedIds(new Set());
setNewFiles([]);
} catch (e) {
alert("가게 정보를 불러오는데 실패했습니다.");
return;
}
})();
Comment thread
jjjsun marked this conversation as resolved.
}, [storeId]);

const toggleDay = (day: string) => {
Expand Down Expand Up @@ -117,19 +117,12 @@ const StoreSettings: React.FC<StoreSettingsProps> = ({ storeId }) => {
e.target.value = "";
};

const handleDeleteImage = (tableId?: number, fileIndex?: number) => {
if (tableId !== undefined) {
setTableImages((prev) => prev.filter((img) => img.tableId !== tableId));
setDeletedIds((prev) => [...prev, tableId]);
}

if (fileIndex !== undefined) {
setNewFiles((prev) => prev.filter((_, i) => i !== fileIndex));
}
const handleDeleteImage = (fileIndex: number) => {
setNewFiles((prev) => prev.filter((_, i) => i !== fileIndex));
};

const inputStyle =
"w-full border border-gray-200 rounded-lg p-4 mt-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all placeholder:text-gray-400";
"w-full border border-gray-200 rounded-lg p-4 mt-2 text-sm text-black focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all placeholder:text-gray-400";
const labelStyle = "block text-md text-gray-700";
const sectionStyle = "bg-white border border-gray-200 rounded-lg p-8 mb-6";

Expand All @@ -140,7 +133,7 @@ const StoreSettings: React.FC<StoreSettingsProps> = ({ storeId }) => {
return (
<div className="max-w-7xl mx-auto px-8 py-10">
<section className={sectionStyle}>
<h3 className="text-lg mb-8">기본 정보</h3>
<h3 className="text-lg mb-8 text-black font-medium">기본 정보</h3>
<div className="space-y-6">
<div>
<label htmlFor="store-name" className={labelStyle}>
Expand Down Expand Up @@ -226,7 +219,7 @@ const StoreSettings: React.FC<StoreSettingsProps> = ({ storeId }) => {
</section>

<section className={sectionStyle}>
<h3 className="text-lg mb-8">영업 시간</h3>
<h3 className="text-lg mb-8 text-black font-medium">영업 시간</h3>
<div className="space-y-6 mb-8">
<div>
<label className={labelStyle}>오픈 시간</label>
Expand Down Expand Up @@ -282,15 +275,15 @@ const StoreSettings: React.FC<StoreSettingsProps> = ({ storeId }) => {
</section>

<section className={sectionStyle}>
<h3 className="text-lg mb-8">예약 정책</h3>
<h3 className="text-lg mb-8 text-black font-medium">예약 정책</h3>
<div className="space-y-8">
<div>
<label className={labelStyle}>예약 가능 기간</label>
<div className="relative mt-2">
<select
value={reservationPeriod}
onChange={(e) => setReservationPeriod(e.target.value)}
className="cursor-pointer w-full border border-gray-200 rounded-lg p-4 text-sm appearance-none outline-none focus:ring-2 focus:ring-blue-500"
className="cursor-pointer w-full border border-gray-200 rounded-lg p-4 appearance-none outline-none focus:ring-2 focus:ring-blue-500"
>
<option>당일만</option>
<option>1주일 전까지</option>
Expand Down Expand Up @@ -356,45 +349,65 @@ const StoreSettings: React.FC<StoreSettingsProps> = ({ storeId }) => {
</section>

<section className={sectionStyle}>
<h3 className="text-lg mb-8">식당 테이블 이미지</h3>

<div className="mb-6">
<input
id="file-upload"
type="file"
multiple
accept="image/*"
onChange={handleFileChange}
className="hidden"
/>
<div className="mb-7">
<p>• 최대 용량: 1MB</p>
<p>• 형식: JPG(JPEG), PNG</p>
<div className="flex items-center justify-between gap-4 mb-6">
<div>
<h3 className="text-lg text-black font-medium">
식당 테이블 이미지
</h3>
<p className="mt-2 text-sm text-gray-600">
• 최대 용량: 1MB • 형식: JPG(JPEG), PNG
</p>
</div>

<div className="shrink-0">
<input
id="file-upload"
type="file"
multiple
accept="image/jpeg,image/jpg,image/png"
onChange={handleFileChange}
className="hidden"
/>
<label
htmlFor="file-upload"
className="cursor-pointer rounded-lg shadow-sm text-black border px-4 py-2 hover:bg-blue-100 hover:text-blue-600 hover:font-medium transition-colors inline-block"
>
이미지 추가하기
</label>
</div>
<label
htmlFor="file-upload"
className="cursor-pointer rounded-xl text-gray-700 border shadow-sm px-4 py-2 hover:bg-blue-100 hover:text-blue-600 transition-colors inline-block"
>
이미지 추가하기
</label>
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{tableImages.map((img) => (
<div key={img.tableId} className="relative">
<img
src={img.tableImageUrl}
alt={`테이블 이미지 ${img.tableId + 1}`}
className="w-full h-32 object-cover rounded-lg"
/>
<button
onClick={() => handleDeleteImage(img.tableId)}
className="absolute top-1 right-1 bg-black text-white text-xs px-2 py-1 rounded"
>
삭제
</button>
</div>
))}
{tableImages
.filter((img) => !deletedIds.has(img.tableImageId))
.map((img, idx) => (
<div key={img.tableImageId} className="relative">
<img
src={img.tableImageUrl}
alt={`테이블 이미지 ${idx + 1}`}
className="w-full h-32 object-cover rounded-lg"
/>
<button
type="button"
aria-label={`테이블 이미지 ${idx + 1} 삭제`}
onClick={() => {
const ok = window.confirm(
"정말로 이 사진을 삭제하시겠습니까?\n(설정 저장을 눌러야 최종 반영됩니다)",
);
if (!ok) return;

setDeletedIds((prev) => {
const next = new Set(prev);
next.add(img.tableImageId);
return next;
});
}}
className="absolute top-2 right-2 bg-red-500 text-white text-xs px-2 py-1 rounded hover:bg-red-400 cursor-pointer transition"
>
삭제
</button>
Comment thread
jjjsun marked this conversation as resolved.
</div>
))}

{newFiles.map((file, index) => {
const previewUrl = URL.createObjectURL(file);
Expand All @@ -408,10 +421,11 @@ const StoreSettings: React.FC<StoreSettingsProps> = ({ storeId }) => {
/>
<button
type="button"
onClick={() => handleDeleteImage(undefined, index)}
className="cursor-pointer absolute top-1 right-1 bg-gray-500 text-white text-xs px-2 py-1 rounded-lg hover:bg-gray-800 transition-colors"
aria-label={`새 이미지 ${index + 1} 삭제`}
onClick={() => handleDeleteImage(index)}
className="absolute top-2 right-2 bg-red-500 text-white text-xs px-2 py-1 rounded hover:bg-red-400 cursor-pointer transition"
>
<X className="size-4" />
삭제
</button>
Comment thread
jjjsun marked this conversation as resolved.
</div>
);
Expand Down Expand Up @@ -458,32 +472,26 @@ const StoreSettings: React.FC<StoreSettingsProps> = ({ storeId }) => {
return;
}
try {
if (deletedIds.length > 0) {
await deleteTableImages(storeId, deletedIds);
if (deletedIds.size > 0) {
await deleteTableImages(storeId, Array.from(deletedIds));
}
} catch (e) {
alert("일부 이미지 삭제에 실패했습니다.");
alert("이미지 삭제에 실패했습니다.");
return;
}
if (newFiles.length > 0) {
for (const file of newFiles) {
try {
await uploadTableImages(storeId, [file]);
} catch (e) {
console.error(`${file.name} 업로드 실패`);
}
}
}
setNewFiles([]);
setDeletedIds([]);
try {
const res = await getStore(storeId);
if (res.data.result.tableImages) {
setTableImages(res.data.result.tableImages);
if (newFiles.length > 0) {
await uploadTableImages(storeId, newFiles);
}
} catch (e) {
console.error("저장 후 데이터 갱신 실패", e);
alert("이미지 업로드에 실패했습니다.");
return;
}

const imgs = await getTableImages(storeId);
setTableImages(imgs);
setDeletedIds(new Set());
setNewFiles([]);
alert("설정이 저장되었습니다.");
}}
className="cursor-pointer bg-blue-600 text-white px-12 py-4 rounded-xl shadow-lg hover:bg-blue-700 active:scale-95 transition-all text-lg"
Expand Down
3 changes: 2 additions & 1 deletion src/components/reservation/modals/ReservationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ export default function ReservationModal({
return { msg: "날짜를 먼저 선택해주세요", times: [] as string[] };
if (timesQuery.isLoading)
return { msg: "예약 가능시간 기다리는중..", times: [] };
if (timesQuery.isError) return { msg: "서버 조회 실패", times: [] };
if (timesQuery.isError)
return { msg: "휴무일 입니다. 다른 날짜를 선택해주세요", times: [] };
Comment thread
jjjsun marked this conversation as resolved.
if (times.length === 0)
return { msg: "예약 가능한 시간이 없어요", times: [] };
return { msg: null as string | null, times };
Expand Down