relax
"use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useTranslations, useLocale } from "next-intl";
import useSWR from "swr";
import { useAuthStore } from "@/app/store/auth";
import useNativeStatus from "@/app/hooks/useNativeStatus";
import { parseJwt } from "@/app/lib/jwtUtils";
import { fetcher, fetcherOnly } from "@/app/utils/fetchers";
import _axios from "@/app/lib/axios";
import dayjs from "dayjs";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
dayjs.extend(isSameOrBefore);
import Image from "next/image";
import { Modal, Select } from "antd";
import RelaxroomForm from "@/app/components/relaxroom/RelaxroomForm";
import Operating3 from "@/app/components/relaxroom/Operating3";
import icoClose from "@images/icon/ic_close.svg";
import icoNotice from "@images/icon/ic_notice.svg";
const { Option } = Select;
import selectArrow from "@images/icon/icon-24-black-arrow.svg";
import KeyVisualNoApi from "@/app/components/common/keyVisualNoApi/KeyVisual";
import { KeyVisualEmptyNative } from "@/app/components/common/KeyVisualEmptyNative/KeyVisualEmptyNative";
import Loading from "@/app/components/common/loading";
import "./style.scss";
export default function RelaxRoom({ params }) {
const dict = useTranslations();
const currentLocale = useLocale();
const { locale } = params;
const hasHydrated = useAuthStore((state) => state._hasHydrated);
const accessToken = useAuthStore((state) => state.accessToken);
const removeAccessToken = useAuthStore((state) => state.removeAccessToken);
const userInfo = useAuthStore((state) => state.userInfo);
const router = useRouter();
const [user, setUser] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalModify, setIsModalModify] = useState(false);
const [detailModalOpen, setDetailModalOpen] = useState(false);
const [noticeModalOpen, setNoticeModalOpen] = useState(false);
const [scheduleForm, setScheduleForm] = useState(null);
const [selectedRoom, setSelectedRoom] = useState(null);
const [selectedDate, setSelectedDate] = useState(dayjs().format("YYYY-MM-DD"));
const [detailData, setDetailData] = useState(null);
const isNative = useNativeStatus();
const canFetchRoomList = hasHydrated && accessToken;
const canFetchRoomDetails = hasHydrated && accessToken && selectedRoom;
const { data: relaxRoom } = useSWR(
canFetchRoomList ? `/api/v1/sleep/reserve/room-list?lang=${currentLocale}` : null,
fetcher
);
const { data: roomInfoList } = useSWR(
canFetchRoomDetails ? `/api/v1/sleep/reserve/room-info-list?roomId=${selectedRoom}` : null,
fetcher
);
const { data: roomCount, mutate: mutateRoomCount } = useSWR(
canFetchRoomDetails
? `/api/v1/sleep/reserve/count?roomId=${selectedRoom}&reserveDt=${selectedDate}`
: null,
fetcher
);
const { data: roomSimple, mutate: mutateRoomSimple } = useSWR(
canFetchRoomDetails
? `/api/v1/sleep/reserve/simple?roomId=${selectedRoom}&reserveDt=${selectedDate}`
: null,
fetcher
);
const logout = () => {
try {
removeAccessToken();
setUser(null);
router.push("/work/lifein");
} catch (error) {
console.error(error);
}
};
// 예약 관련 작업 후 슬롯 리스트 갱신
const refreshSlotData = async () => {
try {
await Promise.all([mutateRoomCount(), mutateRoomSimple()]);
} catch (error) {
console.error("데이터 갱신 중 오류:", error);
}
};
const deleteSchedule = async () => {
try {
const res = await _axios.get(`/api/v1/sleep/reserve/delete?id=${detailData.id}`);
if (res.status === 200) {
alert(res.data.message);
setDetailModalOpen(false);
} else {
//console.log("res2 : ", res.data.message);
alert(res.data?.message || "예약 취소 실패");
}
} catch (error) {
if ([401, 403].includes(error?.response?.status)) logout();
}
};
// 유저 파싱
useEffect(() => {
if (!hasHydrated) return;
if (accessToken) {
const claims = parseJwt(accessToken);
if (claims) {
setUser(claims);
}
} else {
setUser(null);
}
}, [hasHydrated, accessToken]);
useEffect(() => {
console.log(user, userInfo);
// 권한 체크는 return에서 처리
}, [user, userInfo]);
useEffect(() => {
if (!hasHydrated) return;
if (accessToken) {
const claims = parseJwt(accessToken);
const isMember = claims?.roles?.includes("MEMBER");
if (!isMember) {
alert(dict("work.relaxroom.accessDeniedSecretary"));
router.push("/work/lifein");
return;
}
}
}, [hasHydrated, accessToken]);
// MEMBER인 경우 접근 허용 및 스크롤
useEffect(() => {
if (!hasHydrated) return;
if (accessToken && user) {
const claims = parseJwt(accessToken);
const isMember = claims?.roles?.includes("MEMBER");
if (isMember) {
setTimeout(() => {
const currentScrollY = window.scrollY;
const targetScrollY = window.innerHeight;
window.scrollTo({
top: targetScrollY,
behavior: "smooth",
});
}, 500); // 데이터 로딩 후 스크롤 실행
}
}
}, [hasHydrated, accessToken, user]);
useEffect(() => {
if (relaxRoom && relaxRoom.length > 0 && !selectedRoom) {
setSelectedRoom(relaxRoom[0].id);
}
}, [relaxRoom, selectedRoom, user, userInfo]);
// 항상 당일로 selectedDate 고정
useEffect(() => {
const today = dayjs().format("YYYY-MM-DD");
if (selectedDate !== today) {
setSelectedDate(today);
}
}, [selectedDate]);
const openDetailModal = async (time) => {
try {
const res = await _axios.get(
`/api/v1/sleep/reserve/simple?roomId=${selectedRoom}&reserveDt=${selectedDate}`
);
if (res.status === 200 && res?.data?.data?.id) {
const slotData = (
await _axios.get(
`/api/v1/sleep/reserve/detail?lang=${currentLocale}&id=${res?.data?.data?.id}`
)
).data.data;
setDetailData(slotData);
setDetailModalOpen(true);
} else {
// alert(res.data.message);
}
} catch (e) {
console.error(e);
}
};
// 권한 체크: 로딩 중이거나 권한이 없으면 Loading만 표시
const claims = accessToken ? parseJwt(accessToken) : null;
const isMember = claims?.roles?.includes("MEMBER");
const hasPermission = hasHydrated && accessToken && user && isMember;
const isLoading =
!hasHydrated || (hasHydrated && !accessToken) || (hasHydrated && accessToken && !user);
if (isLoading || !hasPermission) {
return <Loading visible={true} />;
}
return (
<div>
{isNative ? (
<KeyVisualEmptyNative
data={{
mainImg: {
src: "/images/guestservices/bg_work.png",
alt: dict("work.guestservices.title"),
title: dict("work.guestservices.title"),
subtitle: <>{dict("work.guestservices.subtitle")}</>,
},
}}
/>
) : (
<KeyVisualNoApi
data={{
mainImg: {
src: "/images/guestservices/bg_work.png",
alt: dict("work.guestservices.title"),
title: dict("work.guestservices.title"),
subtitle: <>{dict("work.guestservices.subtitle")}</>,
},
}}
/>
)}
<div className="wrap">
<div
className={`xl:mp-20 mx-auto box-border w-full overflow-hidden ${isNative ? "pt-[120px]" : "pt-[60px]"} xl:mx-auto xl:max-w-[1260px] xl:px-[30px] xl:pt-[150px]`}
>
<h3 className="px-[20px] text-center font-chap text-f24 font-normal leading-[120%] tracking-chap64 text-themeBlack xl:text-f32">
{dict("work.guestservices.relaxroom.listTitle")}
</h3>
<p className="text-center text-f14 font-medium text-themeBlack xl:text-f18">
{dict("work.guestservices.relaxroom.listSubTitle")}
</p>
</div>
<div className="mb-[16px] max-w-[1240px] px-5 lg:mx-auto lg:mb-[128px]">
<div className="mb-4 mt-[52px] flex items-center justify-between gap-4 border border-themeLGrey px-5 py-4 lg:px-8 lg:py-5">
<div className="flex flex-wrap items-center lg:gap-2">
<span className="w-full flex-auto text-f14 font-medium lg:w-fit lg:text-f18">
{userInfo?.companyName}
</span>
<span className="w-full flex-auto text-f22 lg:w-fit lg:text-f28">
<span className="text-28 font-bold">{userInfo?.name}</span>님
</span>
</div>
<button
onClick={() => {
// 07:00 이전에는 예약 불가
const now = dayjs();
const today7AM = dayjs().hour(7).minute(0).second(0);
if (now.isBefore(today7AM)) {
alert("매일 오전 07:00부터 예약이 가능합니다.");
return;
}
const firstRoomInfo = roomInfoList?.[0];
if (!firstRoomInfo) return;
setIsModalModify(false);
setScheduleForm({
roomInfoId: firstRoomInfo?.roomInfoId,
reserveDt: dayjs().format("YYYY-MM-DD"), // 항상 당일로 설정
reserveTime: null,
tel: userInfo?.tel || "",
roomId: selectedRoom,
});
setIsModalOpen(true);
}}
className="button-basic group !min-w-0 bg-themeBlack hover:bg-themeWhite"
>
<span className="txt !font-semibold text-themeWhite group-hover:text-themeBlack">
{dict("work.relaxroom.reserveButton")}
</span>
</button>
</div>
<div className="relative min-h-[50vh] space-y-4 overflow-hidden bg-white">
<div className="absolute top-[70px] flex lg:left-0 lg:top-[45px]">
<Select
placeholder={dict("work.relaxroom.roomSelect")}
value={selectedRoom}
onChange={(e) => setSelectedRoom(e)}
className="custom-select-small w-[180px] lg:w-[240px]"
suffixIcon={
<Image width={24} height={24} alt={"icon"} src={selectArrow} className="" />
}
>
{relaxRoom?.map((room) => (
<Option key={room.id} value={room.id} className="meetoption">
{room.location} {room.name}
</Option>
))}
</Select>
</div>
<div
className="absolute right-0 top-[70px] !mt-0 flex cursor-pointer items-center gap-[4px] text-f12 font-medium text-[#666] lg:top-[55px] lg:text-f14"
onClick={() => setNoticeModalOpen(true)}
>
{dict("work.relaxroom.rules")}
<Image
src={icoNotice}
width={18}
height={18}
alt="notice"
className="h-[14px] w-[14px] lg:h-[18px] lg:w-[18px]"
/>
</div>
<div className="!mb-[40px] !mt-0 flex items-center justify-center gap-2 pt-6 lg:!mb-0 lg:pt-10">
<span className="text-f24 font-bold text-themeBlack">
{dayjs().format("YYYY년 MM월 DD일")}
</span>
</div>
{/* 07:00 이전 안내 메시지 */}
{/* {dayjs().isBefore(dayjs().hour(7).minute(0).second(0)) && (
<div className="text-center mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-orange-600 font-medium">
매일 오전 07:00부터 예약이 가능합니다.
</p>
</div>
)} */}
{roomInfoList?.length > 0 ? (
<ul className="grid grid-cols-2 gap-5 lg:mt-[20px] lg:grid-cols-3">
{(() => {
// 각 조건 체크 및 디버깅
if (!roomInfoList) {
return (
<div className="col-span-full py-8 text-center">
룸 정보 목록을 불러오는 중...
</div>
);
}
if (!relaxRoom) {
return (
<div className="col-span-full py-8 text-center">룸 목록을 불러오는 중...</div>
);
}
if (!Array.isArray(roomCount)) {
return (
<div className="col-span-full py-8 text-center">
예약 현황을 불러오는 중...
</div>
);
}
// 모든 조건이 충족된 경우 슬롯 생성 진행
const room = relaxRoom.find((r) => r.id === selectedRoom);
// room이 없는 경우 에러 방지
if (!room || !room.startTime || !room.endTime) {
return (
<div className="col-span-full py-8 text-center">
룸 정보를 찾을 수 없습니다.
</div>
);
}
const start = dayjs(`${selectedDate} ${room.startTime}`);
const end = dayjs(`${selectedDate} ${room.endTime}`);
// 시간 파싱 검증
if (!start.isValid() || !end.isValid()) {
return (
<div className="col-span-full py-8 text-center">
시간 형식이 올바르지 않습니다.
</div>
);
}
const slots = [];
let current = start.clone(); // clone으로 복사본 생성
let slotCount = 0;
// while 루프 조건 개선
while (current.clone().add(50, "minute").isSameOrBefore(end)) {
slotCount++;
const timeStr = current.format("HH:mm");
const cntObj = roomCount.find(
(c) => c.reserveTime && c.reserveTime.startsWith(timeStr)
) || { reserveCount: 0 };
const remaining = room.roomCount - cntObj.reserveCount;
const isReserved = cntObj.reserveCount > 0;
const slotStartTime = dayjs(`${selectedDate} ${timeStr}`);
const now = dayjs();
const isToday = selectedDate === dayjs().format("YYYY-MM-DD");
// 매일 07:00 이전에는 예약 비활성화
const today7AM = dayjs().hour(7).minute(0).second(0);
const isBeforeActivation = now.isBefore(today7AM);
const isBeforeLimit =
isToday &&
(slotStartTime.isBefore(now.add(0, "minute")) || isBeforeActivation);
// 나의 예약 정보 확인 (roomSimple에서 해당 시간대의 예약이 있는지 확인)
let myReservation = false;
let reservedName = null;
if (roomSimple) {
// roomSimple이 배열인 경우 처리
const reservationData = Array.isArray(roomSimple)
? roomSimple.find(
(item) =>
item.reserveTime &&
(item.reserveTime.startsWith(timeStr) ||
item.reserveTime.substring(0, 5) === timeStr)
)
: roomSimple;
if (reservationData && reservationData.reserveTime) {
myReservation =
reservationData.reserveTime.startsWith(timeStr) ||
reservationData.reserveTime.substring(0, 5) === timeStr;
if (myReservation) {
reservedName =
reservationData.roomName ||
reservationData.roomNumberName ||
reservationData.roomNm ||
reservationData.roomNumber;
}
}
}
slots.push(
<li key={timeStr} className={`relative border`}>
{reservedName && reservedName.trim() && (
<div className="absolute left-2 top-2 z-10 bg-black px-2 py-1 text-xs text-white">
{reservedName} 예약 완료
</div>
)}
<div
className={`px-3 pb-6 pt-7 text-center lg:px-5 lg:py-10 ${isBeforeLimit || remaining === 0 ? "opacity-20" : ""}`}
>
<div className="text-f16 font-semibold lg:text-f22">
{timeStr} ~ {current.add(50, "m").format("HH:mm")}
</div>
<div className="mt-[2px] text-f12 font-medium text-themeGray82 lg:mt-1 lg:text-f18">
잔여 Relax Room 수 : {remaining}
</div>
</div>
{(isBeforeLimit && !myReservation) || remaining === 0 ? (
<div className="border-t py-2 text-center">
<div
className={`text-f12 font-medium text-themeGray66 opacity-20 lg:text-f14`}
>
{isBeforeActivation ? "예약 시간 전" : "예약 마감"}
</div>
</div>
) : myReservation ? (
<>
<div
className={`cursor-pointer border-t bg-[#E6E2E1] bg-opacity-30 py-2 text-center`}
onClick={() => openDetailModal(timeStr)}
>
<span className={`underline ${isBeforeLimit ? "opacity-30" : ""}`}>
예약 정보 확인
</span>
</div>
</>
) : (
<div
className="cursor-pointer border-t py-1 text-center text-f12 text-themeGray66 lg:py-2.5 lg:text-f14"
onClick={() => {
// 07:00 이전에는 예약 불가
const now = dayjs();
const today7AM = dayjs().hour(7).minute(0).second(0);
if (now.isBefore(today7AM)) {
alert("매일 오전 07:00부터 예약이 가능합니다.");
return;
}
setScheduleForm({
roomInfoId: roomInfoList[0]?.roomInfoId,
reserveDt: dayjs().format("YYYY-MM-DD"), // 항상 당일로 설정
reserveTime: timeStr,
tel: userInfo?.tel || "",
roomId: selectedRoom,
});
setIsModalModify(false);
setIsModalOpen(true);
}}
>
예약 가능
</div>
)}
</li>
);
current = current.add(1, "hour"); // clone 방식으로 변경
}
// 슬롯이 생성되지 않은 경우 처리
if (slots.length === 0) {
return (
<div className="col-span-full py-8 text-center">
<p>해당 시간대에 예약 가능한 슬롯이 없습니다.</p>
<p className="mt-2 text-sm text-gray-500">
운영시간: {room.startTime} ~ {room.endTime}
</p>
</div>
);
}
return slots;
})()}
</ul>
) : (
<div className="mt-[40px] flex h-[400px] items-center justify-center border border-[#E6E2E1]">
{"예약 가능한 Room이 없습니다."}
</div>
)}
</div>
</div>
</div>
<Modal
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
footer={null}
centered
title={
<h2 className="mb-[4px] ml-[-4px] mt-[20px] text-f24 font-bold text-themeBlack lg:mb-[4px] lg:ml-[16px]">
{isModalModify
? dict("work.forms.relaxroomForm.modifyTitle")
: dict("work.forms.relaxroomForm.title")}
</h2>
}
closeIcon={
<Image
src={icoClose}
width={40}
height={40}
alt="close"
onClick={() => setIsModalOpen(false)}
/>
}
width={800}
className="relaxmodal"
>
<RelaxroomForm
isModify={isModalModify}
formData={scheduleForm}
setFormData={setScheduleForm}
onClose={() => {
setIsModalOpen(false);
}}
logout={logout}
userInfo={userInfo}
dict={dict}
selectedRoom={selectedRoom}
meetingRoom={relaxRoom}
refreshSlotData={refreshSlotData}
selectedRoomInfo={relaxRoom?.find((room) => room.id === selectedRoom)}
reservedTimes={roomCount}
myReservedTime={roomSimple}
/>
</Modal>
{/* 예약 상세 모달 */}
<Modal
open={detailModalOpen}
centered
title={
<div className="mb-[40px] flex flex-col items-start justify-start gap-2.5">
<h2 className="ml-[14px] mt-[20px] text-f24 font-bold text-themeBlack">
{dict("work.relaxroom.reservationInfo")}
</h2>
<h4 className="ml-[14px] mt-[4px] text-themeGray82">
Relax Room 이용 20분 전부터는 예약 수정 또는 취소가 불가능합니다.
</h4>
</div>
}
footer={null}
closeIcon={
<Image
src={icoClose}
width={40}
height={40}
alt="close"
className="lg:h-10 lg:w-10"
onClick={() => {
setDetailModalOpen(false);
}}
/>
}
width={{
xs: "90%",
sm: "80%",
md: "800px",
lg: "800px",
xl: "800px",
xxl: "800px",
}}
className="relaxmodal"
>
{detailData && (
<div className="px-[14px] pb-[20px]">
<table className="w-full border-collapse">
<tbody>
<tr>
<th className="w-[25%] border border-themeGrayCC bg-[#e6e2e180] px-[16px] py-[12px] text-left text-f16 font-medium">
예약자
</th>
<td colSpan={3} className="w-[30%] border border-themeGrayCC px-[16px] py-[12px]">
{detailData.userName} ({detailData.companyName})
</td>
</tr>
<tr>
<th className="w-[25%] border border-themeGrayCC bg-[#e6e2e180] px-[16px] py-[12px] text-left text-f16 font-medium">
Relax Room
</th>
<td className="w-[25%] border border-themeGrayCC px-[16px] py-[12px]">
{detailData.roomName} {detailData.location}
</td>
<th className="w-[25%] border border-themeGrayCC bg-[#e6e2e180] px-[16px] py-[12px] text-left text-f16 font-medium">
Relax Room 좌석
</th>
<td className="w-[25%] border border-themeGrayCC px-[16px] py-[12px]">
{detailData.roomNumberName}
</td>
</tr>
<tr>
<th className="w-[25%] border border-themeGrayCC bg-[#e6e2e180] px-[16px] py-[12px] text-left text-f16 font-medium">
예약시간
</th>
<td className="w-[25%] border border-themeGrayCC px-[16px] py-[12px]">
{detailData.reserveTime.slice(0, 5)} ~ {detailData.reserveTime.slice(0, 3)}50
</td>
<th className="w-[25%] border border-themeGrayCC bg-[#e6e2e180] px-[16px] py-[12px] text-left text-f16 font-medium">
전화번호
</th>
<td className="w-[25%] border border-themeGrayCC px-[16px] py-[12px]">
{detailData.phoneNumber}
</td>
</tr>
</tbody>
</table>
<div className="mt-6 text-center text-[#CC0000]">
Relax Room 이용 시 본인 확인을 하고 있습니다. 사원증 또는 명함을 꼭 지참해주세요.
</div>
{(() => {
// 현재 시간과 예약 시간 비교
const now = dayjs();
const reserveDateTime = dayjs(`${detailData.reserveDt} ${detailData.reserveTime}`);
const isPastTime = now.isAfter(reserveDateTime);
// 과거 시간이 아닐 때만 수정/취소 버튼 표시
if (!isPastTime) {
return (
<div className="mt-[40px] flex justify-center gap-[20px]">
<button
className="mt-[8px] box-border inline-block h-[30px] rounded border border-none border-themeBlack bg-themeBlack p-[6px_12px] text-f12 font-bold text-white lg:h-[37px] lg:p-[8px_16px] lg:text-f14"
onClick={() => {
const userConfirmed = confirm("선택한 예약을 수정하시겠습니까?");
if (userConfirmed) {
// roomNumberName을 기반으로 roomInfoId 찾기
const targetRoomInfo = roomInfoList?.find(
(info) => info.roomName === detailData.roomNumberName
);
setIsModalModify(true);
setScheduleForm({
roomInfoId: targetRoomInfo?.roomInfoId || detailData.roomInfoId,
reserveDt: detailData.reserveDt,
reserveTime: detailData.reserveTime.slice(0, 5), // HH:mm
tel: detailData.phoneNumber,
id: detailData.id, // optional: 수정에 필요하다면
roomId: detailData.roomId || selectedRoom, // 실제 예약된 roomId를 우선 사용
});
setIsModalOpen(true);
setDetailModalOpen(false);
}
}}
>
수정 하기
</button>
<button
className="mt-[8px] box-border inline-block h-[30px] rounded border border-none border-themeBlack bg-themeBlack p-[6px_12px] text-f12 font-bold text-white lg:h-[37px] lg:p-[8px_16px] lg:text-f14"
onClick={() => {
const userConfirmed = confirm("선택한 예약을 취소하시겠습니까?");
if (userConfirmed) {
deleteSchedule();
}
}}
>
예약 취소
</button>
</div>
);
}
// 과거 시간인 경우 안내 메시지 표시
return (
<></>
// <div className="mt-[40px] text-center">
// <p className="text-[#CC0000] font-medium">
// 이용 시간이 지난 예약은 수정하거나 취소할 수 없습니다.
// </p>
// </div>
);
})()}
</div>
)}
</Modal>
<Modal
closable={{ "aria-label": "" }}
open={noticeModalOpen}
centered
title={
<h2 className="mb-[24px] ml-[-4px] mt-[20px] text-f24 font-bold text-themeBlack lg:mb-[40px] lg:ml-[16px]">
{dict("work.relaxroom.rules")}
</h2>
}
footer={null}
closeIcon={
<Image
src={icoClose}
width={40}
height={40}
alt="close"
onClick={() => setNoticeModalOpen(false)}
/>
}
width={800}
>
<Operating3 />
</Modal>
</div>
);
}
meeting
"use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useTranslations, useLocale } from "next-intl";
import { useAuthStore } from "@/app/store/auth";
import Image from "next/image";
import dayjs from "dayjs";
import KeyVisualNoApi from "@/app/components/common/keyVisualNoApi/KeyVisual";
import CommonCalendar from "@/app/components/common/Calendar/index";
import ReservationForm from "@/app/components/meetingroom/ReservationForm";
import Operating from "@/app/components/meetingroom/Operating";
import _axios from "@/app/lib/axios";
import useSWR from "swr";
import { Modal, Select } from "antd";
import { parseJwt } from "@/app/lib/jwtUtils";
import { invalidateWorkPagesSWR } from "@/app/lib/swrUtils";
import Loading from "@/app/components/common/loading";
import icoClose from "@images/icon/ic_close.svg";
import icoNotice from "@images/icon/ic_notice.svg";
const { Option } = Select;
import "./style.scss";
import useNativeStatus from "@/app/hooks/useNativeStatus";
import { fetcher } from "@/app/utils/fetchers";
export default function MeetingRoom({ params }) {
const dict = useTranslations();
const currentLocale = useLocale();
const { locale } = params;
const hasHydrated = useAuthStore((state) => state._hasHydrated);
const accessToken = useAuthStore((state) => state.accessToken);
const removeAccessToken = useAuthStore((state) => state.removeAccessToken);
const userInfo = useAuthStore((state) => state.userInfo);
const router = useRouter();
const [user, setUser] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalModify, setIsModalModify] = useState(false);
const [noticeModalOpen, setNoticeModalOpen] = useState(false);
const [scheduleForm, setScheduleForm] = useState(null);
const [detailModalOpen, setDetailModalOpen] = useState(false);
const [detailModalContent, setDetailModalContent] = useState(null);
const [detailRoom, setDetailRoom] = useState(null);
const [detailId, setDetailId] = useState(null);
const [selectedLocation, setSelectedLocation] = useState("");
const [locationList, setLocationList] = useState([]);
const [roomListByLocation, setRoomListByLocation] = useState([]);
const [selectedRoom, setSelectedRoom] = useState(null);
const [scheduleList, setScheduleList] = useState([]);
const isNative = useNativeStatus();
const shouldFetchRoomList = hasHydrated && accessToken;
const shouldFetchRoomDetail = hasHydrated && accessToken && selectedRoom;
const { data: meetingRoom } = useSWR(
shouldFetchRoomList ? `/api/v1/meeting/room-list?isVip=N` : null,
fetcher
);
const { data: roomList, mutate: mutateRoomList } = useSWR(
shouldFetchRoomDetail
? `/api/v1/meeting?roomId=${selectedRoom}&isVip=N&lang=${currentLocale}`
: null,
fetcher
);
const mutateRoomDetail = async () => {
const res = await _axios.post("/api/v1/meeting/detail", {
id: detailId?.resource?.id,
resveDate: detailId?.resource?.resveDate,
companyId: detailId?.resource?.companyId,
roomId: detailId?.resource?.roomId,
});
if (res.data.success) {
setDetailModalContent(res.data.data);
const res2 = await _axios.get(`/api/v1/meeting/room-info?roomId=${selectedRoom}`);
if (res2.data.success) {
setDetailRoom(res2.data.data);
}
}
};
const logout = () => {
try {
removeAccessToken();
setUser(null);
sessionStorage.removeItem("authPageScrolled");
// 현재 페이지가 /work가 아닌 경우에만 리다이렉트
if (window.location.pathname !== "/work") {
alert(dict("work.executiveroom.accessDenied"));
router.push("/work/lifein");
}
} catch (error) {
console.error(error);
}
};
const deleteSchedule = async () => {
const payload = {
id: detailModalContent?.id, // 회의실 예약 id
userId: user?.id, // 예약자 id값
roomId: selectedRoom,
paymentType: detailModalContent?.paymentType === "무료 예약" ? "free" : "paid",
resveDate: detailModalContent?.resveDate,
};
try {
const res = await _axios.post("/api/v1/meeting/cancel", payload, {});
if (res.status === 200) {
alert(res.data.message);
setIsModalOpen(false);
setDetailModalOpen(false);
mutateRoomDetail();
} else {
alert(error?.response?.data.message);
}
} catch (error) {
alert(error?.response?.data.message);
} finally {
mutateRoomList();
mutateRoomDetail();
// SWR 캐시 무효화하여 자동으로 데이터 재요청
invalidateWorkPagesSWR();
}
};
useEffect(() => {
if (detailId?.resource?.id) mutateRoomDetail();
}, [detailId]);
useEffect(() => {
// console.log(user, userInfo);
if (!roomList) {
}
if (!roomList || !user || !userInfo) return;
const mapped = roomList.map((item) => {
const isOwner = item.companyId === userInfo.companyId;
return {
title: isOwner
? `${item.paymentType} ${item.resveStartTime} ~ ${item.resveEndTime} ${item.reserver}(${item.companyName})`
: `${item.resveStartTime} ~ ${item.resveEndTime} 예약됨`,
start: new Date(`${item.resveDate}T${item.resveStartTime}`),
end: new Date(`${item.resveDate}T${item.resveEndTime}`),
resource: item,
isOwner,
};
});
setScheduleList(mapped);
}, [roomList, user, userInfo]);
// 로그인 후 순차적으로 API 호출하는 함수
const initializeDataAfterLogin = async () => {
try {
// 1. location-list 먼저 받기
const locationRes = await _axios.get("/api/v1/meeting/location-list");
const locationData = locationRes.data?.data || [];
setLocationList(locationData);
if (locationData.length > 0) {
const firstLocation = locationData[0].code;
setSelectedLocation(firstLocation);
// 2. room-list 받기
const roomRes = await _axios.get(
`/api/v1/meeting/room-list?isVip=N&location=${firstLocation}`
);
const roomData = roomRes.data?.data || [];
setRoomListByLocation(roomData);
if (roomData.length > 0) {
const firstRoomId = roomData[0].id;
setSelectedRoom(firstRoomId);
// 3. meeting 데이터 받기
const meetingRes = await _axios.get(
`/api/v1/meeting?roomId=${firstRoomId}&isVip=N&lang=${currentLocale}`
);
const meetingData = meetingRes.data?.data || [];
// meeting 데이터는 SWR이 자동으로 처리하므로 여기서는 호출만 함
}
}
} catch (error) {
console.error("API 초기화 중 오류 발생:", error);
}
};
const canSelectDateBefore =
user?.roles?.includes("SUPER_ADMIN") ||
user?.roles?.includes("OFFICE_ADMIN") ||
user?.roles?.includes("NORMAL_ADMIN");
// 유저 파싱
useEffect(() => {
if (!hasHydrated) return;
if (accessToken) {
const claims = parseJwt(accessToken);
if (claims) {
setUser(claims);
}
} else {
setUser(null);
}
}, [hasHydrated, accessToken]);
useEffect(() => {
if (!hasHydrated) return;
if (accessToken && user) {
const claims = parseJwt(accessToken);
const isOfficeSecretaryAdmin = claims?.roles?.includes("OFFICE_SECRETARY_ADMIN");
if (isOfficeSecretaryAdmin) {
// 로그인 후 API 순차 호출
initializeDataAfterLogin();
setTimeout(() => {
const currentScrollY = window.scrollY;
const targetScrollY = window.innerHeight;
window.scrollTo({
top: targetScrollY,
behavior: "smooth",
});
}, 500); // 데이터 로딩 후 스크롤 실행
} else {
alert(dict("work.meetingroom.accessDenied"));
router.push("/work/lifein");
}
}
}, [hasHydrated, accessToken, user, router, currentLocale]);
useEffect(() => {
if (meetingRoom && meetingRoom.length > 0 && !selectedRoom) {
setSelectedRoom(meetingRoom[0].id);
}
}, [meetingRoom]);
useEffect(() => {
if (!selectedLocation) return;
_axios.get(`/api/v1/meeting/room-list?isVip=N&location=${selectedLocation}`).then((res) => {
const data = res.data?.data || [];
setRoomListByLocation(data);
if (data.length > 0) setSelectedRoom(data[0].id);
});
}, [selectedLocation]);
const handleEventClick = (event) => {
if (event.isOwner) {
setDetailId(event);
setDetailModalOpen(true);
} else {
// alert("해당 예약은 상세정보를 확인할 수 없습니다.");
}
};
// 권한 체크: 로딩 중이거나 권한이 없으면 Loading만 표시
const claims = accessToken ? parseJwt(accessToken) : null;
const isOfficeSecretaryAdmin = claims?.roles?.includes("OFFICE_SECRETARY_ADMIN");
const hasPermission = hasHydrated && accessToken && user && isOfficeSecretaryAdmin;
const isLoading =
!hasHydrated || (hasHydrated && !accessToken) || (hasHydrated && accessToken && !user);
if (isLoading || !hasPermission) {
return <Loading visible={true} />;
}
return (
<div>
{isNative ? null : (
<KeyVisualNoApi
data={{
mainImg: {
src: "/images/guestservices/bg_work.png",
alt: dict("work.guestservices.title"),
title: dict("work.guestservices.title"),
subtitle: <>{dict("work.guestservices.subtitle")}</>,
},
}}
/>
)}
<div className="wrap">
<div
className={`xl:mp-20 mx-auto box-border w-full overflow-hidden ${isNative ? "pt-[120px]" : "pt-[60px]"} xl:mx-auto xl:max-w-[1260px] xl:px-[30px] xl:pt-[150px]`}
>
<h3 className="px-[20px] text-center font-chap text-f24 font-normal leading-[120%] tracking-chap64 text-themeBlack xl:text-f32">
{dict("work.guestservices.meetingroom.listTitle")}
</h3>
<p className="text-center text-f14 font-medium text-themeBlack xl:text-f18">
{dict("work.guestservices.meetingroom.listSubTitle")}
</p>
</div>
<div className="mb-24 max-w-[1240px] px-5 lg:mx-auto lg:mb-48 lg:px-0">
<div className="mb-4 mt-[52px] flex items-center justify-between gap-4 border border-themeLGrey px-5 py-4 lg:px-8 lg:py-5">
<div className="flex flex-col flex-wrap items-start lg:flex-row lg:items-center lg:gap-2">
<span className="flex-auto text-f14 font-medium lg:text-f18">
{userInfo?.companyName}
</span>
<span className="flex-auto text-f22 lg:text-f28">
<span className="text-28 font-bold">{userInfo?.name}</span>
{currentLocale === "ko" && "님"}
</span>
</div>
<button
onClick={() => {
setIsModalModify(false);
setScheduleForm({
roomId: selectedRoom,
companyId: userInfo.companyId ?? 119,
reserver: userInfo.name,
email: userInfo.email,
tel: userInfo.tel,
paymentType: "paid",
status: "gs0101",
resveDate: null,
resveStartTime: null,
resveEndTime: null,
content: null,
numberVisitors: null,
note: null,
realUser: null,
});
setIsModalOpen(true);
}}
className="txt button-basic group !min-w-0 rounded bg-themeBlack px-3 py-2 text-f12 font-semibold text-white group-hover:text-themeWhite lg:px-4 lg:py-2 lg:text-f14"
>
<span className="txt !font-semibold text-themeWhite group-hover:!text-themeBlack">
{dict("work.meetingroom.reserveButton")}
</span>
</button>
</div>
<div className="relative overflow-hidden bg-white">
<div>
<div className="absolute left-0 top-[42px] flex w-full justify-center gap-[8px] lg:top-[24px] lg:w-auto lg:justify-start">
<Select
value={selectedLocation}
onChange={(value) => setSelectedLocation(value)}
label={dict("work.guestservices.meetingroom.select")}
placeholder={dict("work.meetingroom.officeSelect")}
className="meetselect w-[100px]"
style={{ width: 95 }}
>
{locationList.map((loc) => (
<Option key={loc.code} value={loc.code} className="meetoption">
{loc.location}
</Option>
))}
</Select>
<Select
value={selectedRoom ? String(selectedRoom) : ""}
onChange={(value) => setSelectedRoom(Number(value))}
label={dict("work.guestservices.meetingroom.select")}
placeholder={dict("work.meetingroom.roomSelect")}
className="meetselect w-[120px]"
style={{ width: 180 }}
>
{roomListByLocation.map((room) => (
<Option key={room.id} value={String(room.id)} className="meetoption">
{room.roomName}
</Option>
))}
</Select>
</div>
<div className="absolute left-0 top-[80px] flex items-center justify-center gap-[16px] lg:left-auto lg:right-0 lg:top-[10px] lg:gap-[20px]">
<div className="flex items-center gap-[4px] text-f12 font-semibold text-[#666] lg:text-f14">
<span className="inline-block h-[16px] w-[16px] bg-[#BF754D]"> </span>{" "}
{dict("work.meetingroom.legend.tentative")}
</div>
<div className="flex items-center gap-[4px] text-f12 font-semibold text-[#666] lg:text-f14">
<span className="inline-block h-[16px] w-[16px] bg-[#798CB1]"> </span>{" "}
{dict("work.meetingroom.legend.confirmed")}
</div>
</div>
<div
className="absolute right-0 top-[80px] flex cursor-pointer items-center gap-[4px] text-f12 font-medium text-[#666] lg:top-[38px] lg:text-f14"
onClick={() => {
setNoticeModalOpen(true);
}}
>
{dict("work.meetingroom.rules")}
<Image
src={icoNotice}
width={18}
height={18}
alt="notice"
className="lg:w-18 lg:h-18"
/>
</div>
</div>
<CommonCalendar
events={scheduleList}
onSelectEvent={handleEventClick}
canSelectDateBefore={canSelectDateBefore}
onCreateReservation={(dateStr) => {
const today = dayjs().startOf("day");
const selected = dayjs(dateStr).startOf("day");
// canSelectDateBefore가 false일 때만 오늘 날짜 체크
if (!canSelectDateBefore && selected.isSame(today)) {
alert(dict("work.meetingroom.sameDayReservation"));
return; // 모달은 열지 않음(EAST: 02-6444-7510 / WEST: 02-6444-7512)
}
// canSelectDateBefore가 false일 때는 내일 이후 예약 가능
setIsModalModify(false);
setScheduleForm({
roomId: selectedRoom,
companyId: userInfo.companyId ?? 119,
reserver: userInfo.name,
email: userInfo.email,
tel: userInfo.tel,
paymentType: "paid",
status: "gs0101",
resveDate: dateStr,
resveStartTime: null,
resveEndTime: null,
content: null,
numberVisitors: null,
note: null,
realUser: null,
});
setIsModalOpen(true);
}}
/>
</div>
</div>
</div>
<Modal
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
footer={null}
centered
title={
<h2 className="mb-[24px] ml-[-4px] mt-[20px] text-f24 font-bold text-themeBlack lg:mb-[40px] lg:ml-[16px]">
{isModalModify
? dict("work.forms.meetingroomForm.modifyTitle")
: dict("work.forms.meetingroomForm.title")}
</h2>
}
closeIcon={
<Image
src={icoClose}
width={40}
height={40}
alt="close"
className="lg:h-10 lg:w-10"
onClick={() => {
setIsModalOpen(false);
}}
/>
}
width={{
xs: "90%",
sm: "80%",
md: "70%",
lg: "800px",
xl: "50%",
xxl: "40%",
}}
>
<ReservationForm
isModify={isModalModify}
formData={scheduleForm}
setFormData={setScheduleForm}
onClose={() => {
setIsModalOpen(false);
mutateRoomList();
mutateRoomDetail();
// SWR 캐시 무효화하여 자동으로 데이터 재요청
invalidateWorkPagesSWR();
}}
logout={() => logout()}
userInfo={userInfo}
dict={dict}
selectedRoom={selectedRoom}
setSelectedRoom={setSelectedRoom}
selectedLocation={selectedLocation}
setSelectedLocation={setSelectedLocation}
locationList={locationList}
roomListByLocation={roomListByLocation}
meetingRoom={meetingRoom}
/>
</Modal>
<Modal
closable={{ "aria-label": "" }}
open={noticeModalOpen}
centered
title={
<h2 className="mb-[24px] ml-[-4px] mt-[20px] text-f24 font-bold text-themeBlack lg:mb-[40px] lg:ml-[16px]">
{dict("work.meetingroom.rules")}
</h2>
}
footer={null}
closeIcon={
<Image
src={icoClose}
width={40}
height={40}
alt="close"
className="lg:h-10 lg:w-10"
onClick={() => {
setNoticeModalOpen(false);
}}
/>
}
width={{
xs: "90%",
sm: "80%",
md: "800px",
lg: "800px",
xl: "800px",
xxl: "800px",
}}
>
<Operating />
</Modal>
{/* 예약 상세 모달 */}
<Modal
closable={{ "aria-label": "" }}
open={detailModalOpen}
centered
title={
<div className="mb-[24px] flex flex-col items-start justify-start gap-2.5 lg:mb-[40px]">
<h2 className="ml-[-4px] mt-[20px] text-f24 font-bold text-themeBlack lg:ml-[14px]">
{dict("work.meetingroom.reservationInfo")}
</h2>
<h4 className="ml-[-4px] mt-[4px] text-themeGray82 lg:ml-[14px]">
{dict("work.meetingroom.reservationNotice")}
</h4>
</div>
}
footer={null}
closeIcon={
<Image
src={icoClose}
width={40}
height={40}
alt="close"
className="lg:h-10 lg:w-10"
onClick={() => {
setDetailModalOpen(false);
}}
/>
}
width={{
xs: "90%",
sm: "80%",
md: "800px",
lg: "800px",
xl: "800px",
xxl: "800px",
}}
>
{detailModalContent && (
<div className="px-[-4px] pb-[20px] lg:px-[14px]">
<table className="hidden w-full border-collapse lg:block">
<tbody>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.meetingName")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.content}
</td>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationType")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.paymentType}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.meetingRoom")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.roomName} {detailModalContent?.location}
</td>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.realUser")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUser} {detailModalContent?.companyName}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.realUserPhone")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUserTel || "-"}
</td>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.realUserEmail")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUserEmail || "-"}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationDateTime")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.resveDate} {detailModalContent?.resveStartTime} ~{" "}
{detailModalContent?.resveEndTime}
</td>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.attendees")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.numberVisitors}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationStatus")}
</th>
<td
className={`w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16 ${detailModalContent?.status === "가예약" ? "text-[#CC0000]" : "text-[#0060CC]"}`}
>
{detailModalContent?.status}
</td>
</tr>
</tbody>
</table>
<table className="w-ull fborder-collapse block lg:hidden">
<tbody>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.meetingName")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.content}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationType")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.paymentType}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.meetingRoom")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.roomName} {detailModalContent?.location}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.realUser")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUser} {detailModalContent?.companyName}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.realUserPhone")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUserTel || "-"}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.realUserEmail")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUserEmail || "-"}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationDateTime")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.resveDate} {detailModalContent?.resveStartTime} ~{" "}
{detailModalContent?.resveEndTime}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.attendees")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.numberVisitors}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationStatus")}
</th>
<td
className={`w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16 ${detailModalContent?.status === "가예약" ? "text-[#CC0000]" : "text-[#0060CC]"}`}
>
{detailModalContent?.status}
</td>
</tr>
</tbody>
</table>
<div className="mt-[20px] bg-[#e6e2e180] px-[12px] py-[20px] lg:px-[20px]">
<p className="font-semibold">
· {dict("work.meetingroom.roomInfo.title")} - {detailModalContent?.location}{" "}
{detailModalContent?.roomName}
</p>
<div className="mt-[8px] flex w-full flex-col gap-[20px] lg:flex-row">
<table className="w-full border-collapse">
<tbody>
{/* <tr>
<th className="border border-themeGrayCC w-[40%] text-f12 font-medium text-left py-[8px] px-[10px]">
호실
</th>
<td className="border border-themeGrayCC w-[60%] text-f12 text-themeGray66 text-left py-[8px] px-[10px]">
{detailRoom?.roomNumber}
</td>
</tr> */}
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.location")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
{detailRoom?.location}
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.maxCapacity")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
{detailModalContent?.roomName === "Meeting Room 2"
? "14명 (최대 " + detailRoom?.capacity + "명까지)"
: detailRoom?.capacity + "명"}
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.operatingHours")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
{detailRoom?.startTime} ~ {detailRoom?.endTime}{" "}
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.paidReservationCost")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
Meeting Room 1 - 시간 당 200,000원(Vat 별도)
<br />
Meeting Room 2 - 시간 당 100,000원(Vat 별도)
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.freeReservationDeduction")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
Meeting Room 1 - 1시간 사용 당 무료 시간 2시간 차감
<br />
Meeting Room 2 - 1시간 사용 당 무료 시간 1시간 차감
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.cancellationPolicy")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
무료예약 : 1 영업일전까지 무료 취소 / 이용 당일 취소 및 변경 불가
<br />
유료예약 : 3 영업일전까지 무료 취소 / 3 영업일 이내 변경 또는 취소시 수수료
20% / 이용 당일 변경 시 수수료 20% , 취소 시 수수료 100%
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.notes")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
1시간 단위로 예약 가능
</td>
</tr>
</tbody>
</table>
<div className="w-full">
<Image
src={detailRoom?.imgPath || "/images/guestservices/img_office.png"}
alt="search_black"
priority
className="w-full lg:w-[330px]"
width={330}
height={222}
/>
</div>
</div>
</div>
<div className="mt-[24px] flex justify-center gap-[20px] lg:mt-[40px]">
{detailModalContent?.status !== "예약 확정" &&
(() => {
// 날짜 비교 로직 추가
const today = dayjs().startOf("day");
const reservationDate = dayjs(detailModalContent?.resveDate).startOf("day");
const daysDiff = reservationDate.diff(today, "day");
// 유료예약일 경우 3일 전까지만, 무료예약일 경우 당일이 아닌 경우에만 버튼 표시
const isFreeReservation = detailModalContent?.paymentType === "무료 예약";
const isPaidReservation = detailModalContent?.paymentType === "유료 예약";
const canModifyOrCancel = isFreeReservation
? daysDiff > 0 // 무료예약: 당일이 아닌 경우
: isPaidReservation
? daysDiff >= 3 // 유료예약: 3일 전까지만
: false;
return canModifyOrCancel ? (
<>
<button
className="mt-[8px] box-border inline-block h-[30px] rounded border border-none border-themeBlack bg-themeBlack p-[6px_12px] text-f12 font-bold text-white lg:h-[37px] lg:p-[8px_16px] lg:text-f14"
onClick={() => {
const userConfirmed = confirm(
dict("work.meetingroom.buttons.modifyConfirm")
);
if (userConfirmed) {
setDetailModalOpen(false);
setScheduleForm({
id: detailModalContent?.id,
userId: user?.id || null,
roomId: selectedRoom || 1,
companyId: userInfo?.companyId || null,
paymentType:
detailModalContent?.paymentType === "무료 예약" ? "free" : "paid",
content: detailModalContent?.content || null,
resveDate: detailModalContent?.resveDate || null,
resveStartTime:
detailModalContent?.resveStartTime?.length === 5
? detailModalContent.resveStartTime + ":00"
: detailModalContent?.resveStartTime,
resveEndTime:
detailModalContent?.resveEndTime?.length === 5
? detailModalContent.resveEndTime + ":00"
: detailModalContent?.resveEndTime,
email: userInfo?.email || null,
reserver: userInfo?.name || null,
tel: userInfo?.tel || null,
status: detailModalContent?.status === "가예약" ? "gs0101" : "gs0101",
note: detailModalContent?.note || null,
numberVisitors: detailModalContent?.numberVisitors || null,
realUser: detailModalContent?.realUser || null,
realUserEmail: detailModalContent?.realUserEmail || null,
realUserTel: detailModalContent?.realUserTel || null,
});
setIsModalModify(true);
setIsModalOpen(true);
mutateRoomList();
//console.log("수정될 내용", scheduleForm);
}
}}
>
{dict("work.meetingroom.buttons.modify")}
</button>
<button
className="mt-[8px] box-border inline-block h-[30px] rounded border border-none border-themeBlack bg-themeBlack p-[6px_12px] text-f12 font-bold text-white lg:h-[37px] lg:p-[8px_16px] lg:text-f14"
onClick={() => {
const userConfirmed = confirm(
dict("work.meetingroom.buttons.cancelConfirm")
);
if (userConfirmed) {
deleteSchedule();
}
}}
>
{dict("work.meetingroom.buttons.cancel")}
</button>
</>
) : null;
})()}
</div>
</div>
)}
</Modal>
</div>
);
}
Executive
"use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useTranslations, useLocale } from "next-intl";
import { useAuthStore } from "@/app/store/auth";
import useSWR from "swr";
import useNativeStatus from "@/app/hooks/useNativeStatus";
import dayjs from "dayjs";
import _axios from "@/app/lib/axios";
import { invalidateWorkPagesSWR } from "@/app/lib/swrUtils";
import { parseJwt } from "@/app/lib/jwtUtils";
import Loading from "@/app/components/common/loading";
import Image from "next/image";
import { Modal, Select } from "antd";
import KeyVisualNoApi from "@/app/components/common/keyVisualNoApi/KeyVisual";
import CommonCalendar from "@/app/components/common/Calendar/index";
import ExecutiveForm from "@/app/components/meetingroom/ExecutiveForm";
import Operating2 from "@/app/components/meetingroom/Operating2";
import icoClose from "@images/icon/ic_close.svg";
import icoNotice from "@images/icon/ic_notice.svg";
const { Option } = Select;
import "./style.scss";
import { fetcher, fetcherOnly } from "@/app/utils/fetchers";
// "SUPER_ADMIN" 슈퍼 관리자
// "NORMAL_ADMIN" 일반 관리자
// "CONTENTS_ADMIN" 리테일 관리자
// "OFFICE_ADMIN" 오피스 관리자
// "OFFICE_SECRETARY_ADMIN" 입주사 총무팀
// "MEMBER" 회원
export default function MeetingRoom({ params }) {
const dict = useTranslations();
const currentLocale = useLocale();
const { locale } = params;
const hasHydrated = useAuthStore((state) => state._hasHydrated);
const accessToken = useAuthStore((state) => state.accessToken);
const removeAccessToken = useAuthStore((state) => state.removeAccessToken);
const userInfo = useAuthStore((state) => state.userInfo);
const router = useRouter();
const [user, setUser] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalModify, setIsModalModify] = useState(false);
const [noticeModalOpen, setNoticeModalOpen] = useState(false);
const [scheduleForm, setScheduleForm] = useState(null);
const [detailModalOpen, setDetailModalOpen] = useState(false);
const [detailModalContent, setDetailModalContent] = useState(null);
const [detailRoom, setDetailRoom] = useState(null);
const [detailId, setDetailId] = useState(null);
const [selectedLocation, setSelectedLocation] = useState("");
const [locationList, setLocationList] = useState([]);
const [roomListByLocation, setRoomListByLocation] = useState([]);
const [selectedRoom, setSelectedRoom] = useState(null);
const [scheduleList, setScheduleList] = useState([]);
const shouldFetchRoomList = hasHydrated && accessToken;
const shouldFetchRoomDetail = hasHydrated && accessToken && selectedRoom;
const isNative = useNativeStatus();
const { data: meetingRoom } = useSWR(
shouldFetchRoomList ? `/api/v1/meeting/room-list?isVip=Y` : null,
fetcherOnly
);
const { data: roomList, mutate: mutateRoomList } = useSWR(
shouldFetchRoomDetail
? `/api/v1/meeting?roomId=${selectedRoom}&isVip=Y&lang=${currentLocale}`
: null,
fetcher
);
const mutateRoomDetail = async () => {
const res = await _axios.post("/api/v1/meeting/detail", {
id: detailId?.resource?.id,
resveDate: detailId?.resource?.resveDate,
companyId: detailId?.resource?.companyId,
roomId: detailId?.resource?.roomId,
});
if (res.data.success) {
setDetailModalContent(res.data.data);
const res2 = await _axios.get(`/api/v1/meeting/room-info?roomId=${selectedRoom}`);
if (res2.data.success) {
setDetailRoom(res2.data.data);
}
}
};
const logout = () => {
try {
removeAccessToken();
setUser(null);
sessionStorage.removeItem("authPageScrolled");
// 현재 페이지가 /work가 아닌 경우에만 리다이렉트
if (window.location.pathname !== "/work/lifein") {
alert(dict("work.executiveroom.accessDenied"));
router.push("/work/lifein");
}
} catch (error) {
console.error(error);
}
};
const deleteSchedule = async () => {
const payload = {
id: detailModalContent?.id, // 회의실 예약 id
userId: user?.id, // 예약자 id값
roomId: selectedRoom,
paymentType: detailModalContent?.paymentType === "무료 예약" ? "free" : "paid",
resveDate: detailModalContent?.resveDate,
};
try {
const res = await _axios.post("/api/v1/meeting/cancel", payload, {});
if (res.status === 200) {
alert(res.data.message);
setIsModalOpen(false);
setDetailModalOpen(false);
mutateRoomDetail();
} else {
alert(error?.response?.data.message);
}
} catch (error) {
alert(error?.response?.data.message);
} finally {
mutateRoomList();
mutateRoomDetail();
// SWR 캐시 무효화하여 자동으로 데이터 재요청
invalidateWorkPagesSWR();
}
};
useEffect(() => {
if (detailId?.resource?.id) mutateRoomDetail();
}, [detailId]);
useEffect(() => {
// console.log(user, userInfo);
if (!roomList) {
}
if (!roomList || !user || !userInfo) return;
const mapped = roomList.map((item) => {
const isOwner = item.companyId === userInfo.companyId;
return {
title: isOwner
? `${item.paymentType} ${item.resveStartTime} ~ ${item.resveEndTime} ${item.reserver}(${item.companyName})`
: `${item.resveStartTime} ~ ${item.resveEndTime} 예약됨`,
start: new Date(`${item.resveDate}T${item.resveStartTime}`),
end: new Date(`${item.resveDate}T${item.resveEndTime}`),
resource: item,
isOwner,
};
});
setScheduleList(mapped);
}, [roomList, user, userInfo]);
// 로그인 후 순차적으로 API 호출하는 함수
const initializeDataAfterLogin = async () => {
try {
// 1. location-list 먼저 받기
const locationRes = await _axios.get("/api/v1/meeting/location-list");
const locationData = locationRes.data?.data || [];
setLocationList(locationData);
if (locationData.length > 0) {
const firstLocation = locationData[0].code;
setSelectedLocation(firstLocation);
// 2. room-list 받기
const roomRes = await _axios.get(
`/api/v1/meeting/room-list?isVip=Y&location=${firstLocation}`
);
const roomData = roomRes.data?.data || [];
setRoomListByLocation(roomData);
if (roomData.length > 0) {
const firstRoomId = roomData[0].id;
setSelectedRoom(firstRoomId);
// 3. meeting 데이터 받기
const meetingRes = await _axios.get(
`/api/v1/meeting?roomId=${firstRoomId}&isVip=Y&lang=${currentLocale}`
);
const meetingData = meetingRes.data?.data || [];
// meeting 데이터는 SWR이 자동으로 처리하므로 여기서는 호출만 함
}
}
} catch (error) {
console.error("API 초기화 중 오류 발생:", error);
}
};
const superAdmin = user?.roles?.includes("SUPER_ADMIN"); // 슈퍼 관리자
const officeAdmin = user?.roles?.includes("OFFICE_ADMIN"); // 오피스 관리자
const normalAdmin = user?.roles?.includes("NORMAL_ADMIN"); // 일반 관리자
const officeSecretaryAdmin = user?.roles?.includes("OFFICE_SECRETARY_ADMIN"); // 입주사 총무팀
const member = user?.roles?.includes("MEMBER"); // 회원
const canSelectDateBefore =
superAdmin || officeAdmin || normalAdmin;
// 유저 파싱
useEffect(() => {
if (!hasHydrated) return;
if (accessToken) {
const claims = parseJwt(accessToken);
if (claims) {
setUser(claims);
}
} else {
setUser(null);
}
}, [hasHydrated, accessToken]);
useEffect(() => {
if (!hasHydrated) return;
if (accessToken && user) {
const claims = parseJwt(accessToken);
const isOfficeSecretaryAdmin = claims?.roles?.includes("OFFICE_SECRETARY_ADMIN");
if (isOfficeSecretaryAdmin) {
// 로그인 후 API 순차 호출
initializeDataAfterLogin();
setTimeout(() => {
const currentScrollY = window.scrollY;
const targetScrollY = window.innerHeight;
window.scrollTo({
top: targetScrollY,
behavior: "smooth",
});
}, 500); // 데이터 로딩 후 스크롤 실행
} else {
alert(dict("work.executiveroom.accessDenied"));
router.push("/work/lifein");
}
}
}, [hasHydrated, accessToken, user, router, currentLocale]);
useEffect(() => {
if (meetingRoom && meetingRoom.length > 0 && !selectedRoom) {
setSelectedRoom(meetingRoom[0].id);
}
}, [meetingRoom]);
useEffect(() => {
if (!selectedLocation) return;
_axios.get(`/api/v1/meeting/room-list?isVip=Y&location=${selectedLocation}`).then((res) => {
const data = res.data?.data || [];
setRoomListByLocation(data);
if (data.length > 0) setSelectedRoom(data[0].id);
});
}, [selectedLocation]);
const handleEventClick = (event) => {
if (event.isOwner) {
setDetailId(event);
setDetailModalOpen(true);
} else {
// alert("해당 예약은 상세정보를 확인할 수 없습니다.");
}
};
// 권한 체크: 로딩 중이거나 권한이 없으면 Loading만 표시
const claims = accessToken ? parseJwt(accessToken) : null;
const isOfficeSecretaryAdmin = claims?.roles?.includes("OFFICE_SECRETARY_ADMIN");
const hasPermission = hasHydrated && accessToken && user && isOfficeSecretaryAdmin;
const isLoading =
!hasHydrated || (hasHydrated && !accessToken) || (hasHydrated && accessToken && !user);
if (isLoading || !hasPermission) {
return <Loading visible={true} />;
}
return (
<div>
{isNative ? null : (
<KeyVisualNoApi
data={{
mainImg: {
src: "/images/guestservices/bg_work.png",
alt: dict("work.guestservices.title"),
title: dict("work.guestservices.title"),
subtitle: <>{dict("work.guestservices.subtitle")}</>,
},
}}
/>
)}
<div className="wrap">
<div
className={`xl:mp-20 mx-auto box-border w-full overflow-hidden ${isNative ? "pt-[120px]" : "pt-[60px]"} xl:mx-auto xl:max-w-[1260px] xl:px-[30px] xl:pt-[150px]`}
>
<h3 className="px-[20px] text-center font-chap text-f24 font-normal leading-[120%] tracking-chap64 text-themeBlack xl:text-f32">
{dict("work.guestservices.executiveroom.listTitle")}
</h3>
<p className="text-center text-f14 font-medium text-themeBlack xl:text-f18">
{dict("work.guestservices.executiveroom.listSubTitle")}
</p>
</div>
<div className="mb-24 max-w-[1240px] px-5 lg:mx-auto lg:mb-48 lg:px-0">
<div className="mb-4 mt-[52px] flex items-center justify-between gap-4 border border-themeLGrey px-5 py-4 lg:px-8 lg:py-5">
<div className="flex flex-col flex-wrap items-start lg:flex-row lg:items-center lg:gap-2">
<span className="flex-auto text-f14 font-medium lg:text-f18">
{userInfo?.companyName}
</span>
<span className="flex-auto text-f22 lg:text-f28">
<span className="text-28 font-bold">{userInfo?.name}</span>
{currentLocale === "ko" && "님"}
</span>
</div>
<button
onClick={() => {
setIsModalModify(false);
setScheduleForm({
roomId: selectedRoom,
companyId: userInfo.companyId ?? 119,
reserver: userInfo.name,
email: userInfo.email,
tel: userInfo.tel,
paymentType: "paid",
status: "gs0101",
resveDate: null,
resveStartTime: null,
resveEndTime: null,
content: null,
numberVisitors: null,
note: null,
realUser: null,
});
setIsModalOpen(true);
}}
className="txt button-basic group !min-w-0 rounded bg-themeBlack px-3 py-2 text-f12 font-semibold text-white group-hover:text-themeWhite lg:px-4 lg:py-2 lg:text-f14"
>
<span className="txt !font-semibold text-themeWhite group-hover:!text-themeBlack">
{dict("work.meetingroom.reserveButton")}
</span>
</button>
</div>
<div className="relative overflow-hidden bg-white">
<div>
<div className="absolute left-0 top-[42px] flex w-full justify-center gap-[8px] lg:top-[24px] lg:w-auto lg:justify-start">
<Select
value={selectedLocation}
onChange={(value) => setSelectedLocation(value)}
label={dict("work.guestservices.meetingroom.select")}
placeholder={dict("work.meetingroom.officeSelect")}
className="meetselect w-[100px]"
style={{ width: 95 }}
>
{locationList.map((loc) => (
<Option key={loc.code} value={loc.code} className="meetoption">
{loc.location}
</Option>
))}
</Select>
<Select
value={selectedRoom ? String(selectedRoom) : ""}
onChange={(value) => setSelectedRoom(Number(value))}
label={dict("work.guestservices.meetingroom.select")}
placeholder={dict("work.meetingroom.roomSelect")}
className="meetselect w-[120px]"
style={{ width: 220 }}
>
{roomListByLocation.map((room) => (
<Option key={room.id} value={String(room.id)} className="meetoption">
{room.roomName}
</Option>
))}
</Select>
</div>
<div className="absolute left-0 top-[80px] flex items-center justify-center gap-[16px] lg:left-auto lg:right-0 lg:top-[10px] lg:gap-[20px]">
<div className="flex items-center gap-[4px] text-f12 font-semibold text-[#666] lg:text-f14">
<span className="inline-block h-[16px] w-[16px] bg-[#BF754D]"> </span>{" "}
{dict("work.meetingroom.legend.tentative")}
</div>
<div className="flex items-center gap-[4px] text-f12 font-semibold text-[#666] lg:text-f14">
<span className="inline-block h-[16px] w-[16px] bg-[#798CB1]"> </span>{" "}
{dict("work.meetingroom.legend.confirmed")}
</div>
</div>
<div
className="absolute right-0 top-[80px] flex cursor-pointer items-center gap-[4px] text-f12 font-medium text-[#666] lg:top-[38px] lg:text-f14"
onClick={() => {
setNoticeModalOpen(true);
}}
>
{dict("work.executiveroom.rules")}
<Image
src={icoNotice}
width={18}
height={18}
alt="notice"
className="lg:w-18 lg:h-18"
/>
</div>
</div>
<CommonCalendar
events={scheduleList}
onSelectEvent={handleEventClick}
canSelectDateBefore={canSelectDateBefore}
onCreateReservation={(dateStr) => {
const today = dayjs().startOf("day");
const selected = dayjs(dateStr).startOf("day");
// canSelectDateBefore가 false일 때만 오늘 날짜 체크
if (!canSelectDateBefore && selected.isSame(today)) {
alert(dict("work.meetingroom.sameDayReservation"));
return; // 모달은 열지 않음(EAST: 02-6444-7510 / WEST: 02-6444-7512)
}
// canSelectDateBefore가 false일 때는 내일 이후 예약 가능
setIsModalModify(false);
setScheduleForm({
roomId: selectedRoom,
companyId: userInfo.companyId ?? 119,
reserver: userInfo.name,
email: userInfo.email,
tel: userInfo.tel,
paymentType: "paid",
status: "gs0101",
resveDate: dateStr,
resveStartTime: null,
resveEndTime: null,
content: null,
numberVisitors: null,
note: null,
realUser: null,
});
setIsModalOpen(true);
}}
/>
</div>
</div>
</div>
<Modal
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
footer={null}
centered
title={
<h2 className="mb-[24px] ml-[-4px] mt-[20px] text-f24 font-bold text-themeBlack lg:mb-[40px] lg:ml-[16px]">
{isModalModify
? dict("work.forms.meetingroomForm.modifyTitle")
: dict("work.forms.meetingroomForm.title")}
</h2>
}
closeIcon={
<Image
src={icoClose}
width={40}
height={40}
alt="close"
className="lg:h-10 lg:w-10"
onClick={() => {
setIsModalOpen(false);
}}
/>
}
width={{
xs: "90%",
sm: "80%",
md: "70%",
lg: "800px",
xl: "50%",
xxl: "40%",
}}
>
<ExecutiveForm
isModify={isModalModify}
formData={scheduleForm}
setFormData={setScheduleForm}
onClose={() => {
setIsModalOpen(false);
mutateRoomList();
mutateRoomDetail();
// SWR 캐시 무효화하여 자동으로 데이터 재요청
invalidateWorkPagesSWR();
}}
logout={() => logout()}
userInfo={userInfo}
dict={dict}
selectedRoom={selectedRoom}
setSelectedRoom={setSelectedRoom}
selectedLocation={selectedLocation}
setSelectedLocation={setSelectedLocation}
locationList={locationList}
roomListByLocation={roomListByLocation}
meetingRoom={meetingRoom}
/>
</Modal>
<Modal
closable={{ "aria-label": "" }}
open={noticeModalOpen}
centered
title={
<h2 className="mb-[24px] ml-[-4px] mt-[20px] text-f24 font-bold text-themeBlack lg:mb-[40px] lg:ml-[16px]">
{dict("work.executiveroom.rules")}
</h2>
}
footer={null}
closeIcon={
<Image
src={icoClose}
width={40}
height={40}
alt="close"
className="lg:h-10 lg:w-10"
onClick={() => {
setNoticeModalOpen(false);
}}
/>
}
width={{
xs: "90%",
sm: "80%",
md: "800px",
lg: "800px",
xl: "800px",
xxl: "800px",
}}
>
<Operating2 />
</Modal>
{/* 예약 상세 모달 */}
<Modal
closable={{ "aria-label": "" }}
open={detailModalOpen}
centered
title={
<div className="mb-[24px] flex flex-col items-start justify-start gap-2.5 lg:mb-[40px]">
<h2 className="ml-[-4px] mt-[20px] text-f24 font-bold text-themeBlack lg:ml-[14px]">
{dict("work.executiveroom.reservationInfo")}
</h2>
<h4 className="ml-[-4px] mt-[4px] text-themeGray82 lg:ml-[14px]">
{dict("work.executiveroom.reservationNotice")}
</h4>
</div>
}
footer={null}
closeIcon={
<Image
src={icoClose}
width={40}
height={40}
alt="close"
className="lg:h-10 lg:w-10"
onClick={() => {
setDetailModalOpen(false);
}}
/>
}
width={{
xs: "90%",
sm: "80%",
md: "800px",
lg: "800px",
xl: "800px",
xxl: "800px",
}}
>
{detailModalContent && (
<div className="px-[-4px] pb-[20px] lg:px-[14px]">
<table className="hidden w-full border-collapse lg:block">
<tbody>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.meetingName")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.content}
</td>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationType")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.paymentType}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.meetingRoom")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.roomName} {detailModalContent?.location}
</td>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reserver")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUser} {detailModalContent?.companyName}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.realUserPhone")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUserTel || "-"}
</td>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.realUserEmail")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUserEmail || "-"}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationDateTime")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.resveDate} {detailModalContent?.resveStartTime} ~{" "}
{detailModalContent?.resveEndTime}
</td>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.attendees")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.numberVisitors}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationStatus")}
</th>
<td
className={`w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16 ${detailModalContent?.status === "가예약" ? "text-[#CC0000]" : "text-[#0060CC]"}`}
>
{detailModalContent?.status}
</td>
</tr>
</tbody>
</table>
<table className="block w-full border-collapse lg:hidden">
<tbody>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.meetingName")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.content}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationType")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.paymentType}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.meetingRoom")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.roomName} {detailModalContent?.location}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reserver")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUser} {detailModalContent?.companyName}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.realUserPhone")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUserTel || "-"}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.realUserEmail")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.realUserEmail || "-"}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationDateTime")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.resveDate} {detailModalContent?.resveStartTime} ~{" "}
{detailModalContent?.resveEndTime}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.attendees")}
</th>
<td className="w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16">
{detailModalContent?.numberVisitors}
</td>
</tr>
<tr>
<th className="w-[20%] border border-themeGrayCC bg-[#e6e2e180] px-[12px] py-[10px] text-left text-f14 font-medium lg:px-[16px] lg:py-[12px] lg:text-f16">
{dict("work.meetingroom.table.reservationStatus")}
</th>
<td
className={`w-[30%] border border-themeGrayCC px-[12px] py-[10px] text-f14 text-themeGray66 lg:px-[16px] lg:py-[12px] lg:text-f16 ${detailModalContent?.status === "가예약" ? "text-[#CC0000]" : "text-[#0060CC]"}`}
>
{detailModalContent?.status}
</td>
</tr>
</tbody>
</table>
<div className="mt-[20px] bg-[#e6e2e180] px-[12px] py-[20px] lg:px-[20px]">
<p className="font-semibold">
· {dict("work.meetingroom.roomInfo.title")} - {detailModalContent?.location}{" "}
{detailModalContent?.roomName}
</p>
<div className="mt-[8px] flex w-full flex-col gap-[20px] lg:flex-row">
<table className="w-full border-collapse">
<tbody>
{/* <tr>
<th className="border border-themeGrayCC w-[40%] text-f12 font-medium text-left py-[8px] px-[10px]">
호실
</th>
<td className="border border-themeGrayCC w-[60%] text-f12 text-themeGray66 text-left py-[8px] px-[10px]">
{detailRoom?.roomNumber}
</td>
</tr> */}
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.location")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
{detailRoom?.location}
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.maxCapacity")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
{detailRoom?.capacity}명
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.operatingHours")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
{detailRoom?.startTime} ~ {detailRoom?.endTime}{" "}
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.paidReservationCost")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
Executive Room 1 - 시간 당 100,000원(VAT 별도)
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.freeReservationDeduction")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
Executive Room 1 - 1시간 사용 당 무료 시간 1시간 차감
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.cancellationPolicy")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
무료예약 : 1 영업일전까지 무료 취소 / 이용 당일 취소 및 변경 불가
<br />
유료예약 : 3 영업일전까지 무료 취소 / 3 영업일 이내 변경 또는 취소시 수수료
20% / 이용 당일 변경 시 수수료 20% , 취소 시 수수료 100%
</td>
</tr>
<tr>
<th className="w-[40%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 font-medium">
{dict("work.meetingroom.roomInfo.notes")}
</th>
<td className="w-[60%] border border-themeGrayCC px-[10px] py-[8px] text-left text-f12 text-themeGray66">
1시간 단위로 예약 가능
</td>
</tr>
</tbody>
</table>
<div className="w-full">
<Image
src={detailRoom?.imgPath || "/images/guestservices/img_office.png"}
alt="search_black"
priority
className="w-full lg:w-[330px]"
width={330}
height={222}
/>
</div>
</div>
</div>
<div className="mt-[24px] flex justify-center gap-[20px] lg:mt-[40px]">
{detailModalContent?.status !== "예약 확정" &&
(() => {
// 날짜 비교 로직 추가
const today = dayjs().startOf("day");
const reservationDate = dayjs(detailModalContent?.resveDate).startOf("day");
const daysDiff = reservationDate.diff(today, "day");
// 유료예약일 경우 3일 전까지만, 무료예약일 경우 당일이 아닌 경우에만 버튼 표시
const isFreeReservation = detailModalContent?.paymentType === "무료 예약";
const isPaidReservation = detailModalContent?.paymentType === "유료 예약";
const canModifyOrCancel = isFreeReservation
? daysDiff > 0 // 무료예약: 당일이 아닌 경우
: isPaidReservation
? daysDiff >= 3 // 유료예약: 3일 전까지만
: false;
return canModifyOrCancel ? (
<>
<button
className="mt-[8px] box-border inline-block h-[30px] rounded border border-none border-themeBlack bg-themeBlack p-[6px_12px] text-f12 font-bold text-white lg:h-[37px] lg:p-[8px_16px] lg:text-f14"
onClick={() => {
const userConfirmed = confirm(
dict("work.meetingroom.buttons.modifyConfirm")
);
if (userConfirmed) {
setDetailModalOpen(false);
setScheduleForm({
id: detailModalContent?.id,
userId: user?.id || null,
roomId: selectedRoom || 1,
companyId: userInfo?.companyId || null,
paymentType:
detailModalContent?.paymentType === "무료 예약" ? "free" : "paid",
content: detailModalContent?.content || null,
resveDate: detailModalContent?.resveDate || null,
resveStartTime:
detailModalContent?.resveStartTime?.length === 5
? detailModalContent.resveStartTime + ":00"
: detailModalContent?.resveStartTime,
resveEndTime:
detailModalContent?.resveEndTime?.length === 5
? detailModalContent.resveEndTime + ":00"
: detailModalContent?.resveEndTime,
email: userInfo?.email || null,
reserver: userInfo?.name || null,
tel: userInfo?.tel || null,
status: detailModalContent?.status === "가예약" ? "gs0101" : "gs0101",
note: detailModalContent?.note || null,
numberVisitors: detailModalContent?.numberVisitors || null,
realUser: detailModalContent?.realUser || null,
realUserEmail: detailModalContent?.realUserEmail || null,
realUserTel: detailModalContent?.realUserTel || null,
});
setIsModalModify(true);
setIsModalOpen(true);
mutateRoomList();
//console.log("수정될 내용", scheduleForm);
}
}}
>
{dict("work.meetingroom.buttons.modify")}
</button>
<button
className="mt-[8px] box-border inline-block h-[30px] rounded border border-none border-themeBlack bg-themeBlack p-[6px_12px] text-f12 font-bold text-white lg:h-[37px] lg:p-[8px_16px] lg:text-f14"
onClick={() => {
const userConfirmed = confirm(
dict("work.meetingroom.buttons.cancelConfirm")
);
if (userConfirmed) {
deleteSchedule();
}
}}
>
{dict("work.meetingroom.buttons.cancel")}
</button>
</>
) : null;
})()}
</div>
</div>
)}
</Modal>
</div>
);
}