diff --git a/app/admin/(components)/Sidebar.tsx b/app/admin/(components)/Sidebar.tsx
index 2f8e026..4ace8cd 100644
--- a/app/admin/(components)/Sidebar.tsx
+++ b/app/admin/(components)/Sidebar.tsx
@@ -1,7 +1,8 @@
-import React, { useEffect } from "react";
+import React, { useEffect, useState } from "react";
import Image from "next/image";
import classNames from "classnames";
import { useRouter, useSearchParams } from "next/navigation";
+import { useQueryClient } from "@tanstack/react-query";
import HintDialog from "@/components/common/Dialog-new/Hint-Dialog-new/Dialog";
import {
@@ -11,6 +12,7 @@ import {
subscribeLinkURL,
} from "@/admin/(consts)/sidebar";
import {
+ getLoginInfo,
getSelectedThemeId,
getStatus,
removeThemeId,
@@ -18,6 +20,7 @@ import {
import { useSelectedThemeReset } from "@/components/atoms/selectedTheme.atom";
import { useDrawerState } from "@/components/atoms/drawer.atom";
import useModal from "@/hooks/useModal";
+import { QUERY_KEY } from "@/queries/getThemeList";
interface Theme {
id: number;
@@ -27,8 +30,6 @@ interface Theme {
}
interface Props {
- adminCode: string;
- shopName: string;
categories: Theme[];
selectedTheme: Theme;
handleClickSelected: (theme: Theme) => void;
@@ -37,6 +38,7 @@ interface Props {
export default function Sidebar(props: Props) {
const router = useRouter();
const resetSelectedTheme = useSelectedThemeReset();
+ const queryClient = useQueryClient();
const [drawer, setDrawer] = useDrawerState();
const { open } = useModal();
@@ -45,12 +47,16 @@ export default function Sidebar(props: Props) {
const searchParams = useSearchParams();
const selectedThemeId = getSelectedThemeId();
const params = new URLSearchParams(searchParams.toString()).toString();
- const {
- adminCode = "",
- shopName = "",
- categories,
- handleClickSelected,
- } = props;
+ const { categories, handleClickSelected } = props;
+ const [loginInfo, setLoginInfo] = useState({
+ adminCode: "",
+ shopName: "",
+ });
+
+ useEffect(() => {
+ const { adminCode, shopName } = getLoginInfo(); // getLoginInfo로 값 가져오기
+ setLoginInfo({ adminCode, shopName }); // 상태 업데이트
+ }, []);
// const handleLogout = () => {
// removeAccessToken();
@@ -62,14 +68,14 @@ export default function Sidebar(props: Props) {
`/admin?themeId=${encodeURIComponent(selectedThemeId)}
`
);
- }, [selectedThemeId]);
+ }, [selectedThemeId, params]);
const navigateToNewTheme = () => {
resetSelectedTheme();
router.push("/admin");
setDrawer({ ...drawer, isOpen: false });
};
- const handleSelectTheme = (theme: Theme) => {
+ const handleSelectTheme = async (theme: Theme) => {
if (drawer.isOpen && !drawer.isSameHint) {
open(HintDialog, {
type: "put",
@@ -80,6 +86,7 @@ export default function Sidebar(props: Props) {
});
} else {
setDrawer({ ...drawer, isOpen: false });
+ await queryClient.invalidateQueries(QUERY_KEY);
handleClickSelected(theme);
}
};
@@ -100,7 +107,7 @@ export default function Sidebar(props: Props) {
- {shopName?.replaceAll(`"`, "")}
+ {loginInfo.shopName?.replaceAll(`"`, "")}
우리 지점 테마
@@ -164,7 +171,7 @@ export default function Sidebar(props: Props) {
관리자 코드
- {adminCode?.replaceAll(`"`, "")}
+ {loginInfo.adminCode?.replaceAll(`"`, "")}
diff --git a/app/admin/(components)/ThemeDrawer/helpers/imageHelpers.ts b/app/admin/(components)/ThemeDrawer/helpers/imageHelpers.ts
index 537d534..fa26d25 100644
--- a/app/admin/(components)/ThemeDrawer/helpers/imageHelpers.ts
+++ b/app/admin/(components)/ThemeDrawer/helpers/imageHelpers.ts
@@ -1,11 +1,29 @@
import imageCompression from "browser-image-compression";
-export const compressImage = async (file: File) => {
- const options = {
- maxSizeMB: 5,
- maxWidthOrHeight: 1920,
- useWebWorker: true,
- };
+export interface FileOptionsType {
+ maxSizeMB: number;
+ maxWidthOrHeight: number;
+ useWebWorker: boolean;
+}
+export const getCompressImage = async (
+ file: File,
+ options: FileOptionsType
+) => {
+ const compressedFile = await compressImage(file, options);
+ try {
+ if (compressedFile.type !== "image/png") {
+ const pngFile = await convertToPng(compressedFile);
+ return pngFile;
+ } else {
+ return compressedFile;
+ }
+ } catch (error) {
+ console.error("Image compression failed", error);
+ return file;
+ }
+};
+
+const compressImage = async (file: File, options: FileOptionsType) => {
try {
const compressedFile = await imageCompression(file, options);
return compressedFile; // compressedFile 반환
@@ -14,7 +32,7 @@ export const compressImage = async (file: File) => {
}
};
-export const convertToPng = async (file: File): Promise =>
+const convertToPng = async (file: File): Promise =>
new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
diff --git a/app/admin/(components)/ThemeDrawer/hooks/useImages.ts b/app/admin/(components)/ThemeDrawer/hooks/useImages.ts
index 20446a9..82127eb 100644
--- a/app/admin/(components)/ThemeDrawer/hooks/useImages.ts
+++ b/app/admin/(components)/ThemeDrawer/hooks/useImages.ts
@@ -14,7 +14,7 @@ import { useToastWrite } from "@/components/atoms/toast.atom";
import { getStatus } from "@/utils/storageUtil";
import { subscribeLinkURL } from "@/admin/(consts)/sidebar";
-import { compressImage, convertToPng } from "../helpers/imageHelpers";
+import { getCompressImage } from "../helpers/imageHelpers";
const useImages = ({
imageType,
@@ -79,19 +79,12 @@ const useImages = ({
const files: File[] = [];
const file = e.target.files[0];
if (file.size > 5 * 1024 * 1024) {
- try {
- const compressedFile = await compressImage(file);
-
- if (compressedFile.type !== "image/png") {
- const pngFile = await convertToPng(compressedFile);
- files.push(pngFile);
- } else {
- files.push(compressedFile);
- }
- } catch (error) {
- console.error("Image compression failed", error);
- files.push(file);
- }
+ const options = {
+ maxSizeMB: 5,
+ maxWidthOrHeight: 1920,
+ useWebWorker: true,
+ };
+ files.push(await getCompressImage(file, options));
} else {
files.push(file);
}
diff --git a/app/admin/(components)/ThemeInfo/Container.tsx b/app/admin/(components)/ThemeInfo/Container.tsx
index eb31974..e50767c 100644
--- a/app/admin/(components)/ThemeInfo/Container.tsx
+++ b/app/admin/(components)/ThemeInfo/Container.tsx
@@ -11,7 +11,7 @@ import ThemeDrawer from "../ThemeDrawer/Container";
import ThemeInfoTitle from "./ThemeInfoTitle";
import ThemeInfoHint from "./ThemeInfoHint";
-import ThemeImage from "./ThemeImage";
+import ThemeImage from "./ThemeTimerImage";
export default function ThemeInfo() {
const { open } = useModal();
diff --git a/app/admin/(components)/ThemeInfo/ThemeImage.tsx b/app/admin/(components)/ThemeInfo/ThemeImage.tsx
deleted file mode 100644
index ff5f4e3..0000000
--- a/app/admin/(components)/ThemeInfo/ThemeImage.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import Image from "next/image";
-import React, { useRef, useState } from "react";
-
-import Tooltip from "@/admin/(components)/Tooltip/Container";
-import Dialog from "@/components/common/Dialog-new/Image-Dialog-new/Dialog";
-import PreviewDialog from "@/components/common/Dialog-new/Preview-Dialog-new/PreviewDialog";
-import useModal from "@/hooks/useModal";
-
-export default function ThemeImage() {
- const QuestionProps = {
- src: "/images/svg/icon_question.svg",
- alt: "gallery_image",
- width: 24,
- height: 24,
- };
-
- const previewProps = {
- src: "/images/svg/icon_preview.svg",
- alt: "NEXT ROOM",
- width: 120,
- height: 120,
- };
- const { open } = useModal();
-
- const imgInputRef = useRef(null);
- const handleAddImageBtnClick = () => {
- // 숨겨진 input 클릭 트리거
- // imgInputRef.current?.click();
- open(Dialog, { type: "put" });
- };
- const handlePreviewImageBtnClick = () => {
- // 숨겨진 input 클릭 트리거
- // imgInputRef.current?.click();
- open(PreviewDialog, { type: "put" });
- };
- const [isHovered, setIsHovered] = useState(false);
-
- return (
-
-
-
타이머 배경
-
- setIsHovered(true)}
- onMouseLeave={() => setIsHovered(false)}
- className="tooptip-button"
- {...QuestionProps}
- />
- {isHovered && }
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/app/admin/(components)/ThemeInfo/ThemeTimerImage.tsx b/app/admin/(components)/ThemeInfo/ThemeTimerImage.tsx
new file mode 100644
index 0000000..f8fdb93
--- /dev/null
+++ b/app/admin/(components)/ThemeInfo/ThemeTimerImage.tsx
@@ -0,0 +1,131 @@
+import Image from "next/image";
+import React, { ChangeEvent, useEffect, useRef, useState } from "react";
+
+import Dialog from "@/components/common/Dialog-new/Image-Dialog-new/Dialog";
+import PreviewDialog from "@/components/common/Dialog-new/Preview-Dialog-new/PreviewDialog";
+import useModal from "@/hooks/useModal";
+import { useTimerImageWrite } from "@/components/atoms/timerImage.atom";
+import { useSelectedTheme } from "@/components/atoms/selectedTheme.atom";
+import { defaultTimerImage, QuestionIconProps } from "@/admin/(consts)/sidebar";
+import DeleteDialog from "@/components/common/Dialog-new/Timer-Image-Delete-Dialog/DeleteDialog";
+import Tooltip from "@/admin/(components)/Tooltip/Container";
+
+import { getCompressImage } from "../ThemeDrawer/helpers/imageHelpers";
+
+export default function ThemeTimerImage() {
+ const [selectedTheme, setSelectedTheme] = useSelectedTheme();
+ const setTimerImage = useTimerImageWrite();
+
+ const [timerImageUrl, setTimerImageUrl] = useState(defaultTimerImage);
+ useEffect(() => {
+ if (selectedTheme.themeImageUrl) {
+ setTimerImageUrl(selectedTheme.themeImageUrl);
+ setSelectedTheme((prev) => ({
+ ...prev,
+ useTimerUrl: true,
+ themeImageUrl: selectedTheme.themeImageUrl,
+ }));
+ return;
+ }
+ setTimerImageUrl(defaultTimerImage);
+ }, [selectedTheme.themeImageUrl]);
+
+ const TimerImageProps = {
+ src: timerImageUrl || "",
+ alt: "NEXT ROOM",
+ width: 120,
+ height: 120,
+ };
+
+ const { open } = useModal();
+
+ const addImageInputRef = useRef(null);
+ const fileReset = () => {
+ if (addImageInputRef.current) {
+ addImageInputRef.current.value = "";
+ }
+ };
+
+ const handleFileInputChange = async (e: ChangeEvent) => {
+ if (!e.target.files) {
+ return;
+ }
+ const file: File = e.target.files[0];
+ if (file.size > 500 * 1024) {
+ const options = {
+ maxSizeMB: 0.5,
+ maxWidthOrHeight: 1000,
+ useWebWorker: true,
+ };
+ const compressedFile = await getCompressImage(file, options);
+ setTimerImage({ timerImage: compressedFile });
+ } else {
+ setTimerImage({ timerImage: file });
+ }
+
+ if (file) {
+ open(Dialog);
+ }
+ fileReset();
+ };
+ const handleAddTimerImageBtnClick = () => {
+ addImageInputRef.current?.click();
+ };
+ const handlePreviewBtnClick = () => {
+ open(PreviewDialog);
+ };
+
+ const handleDelTimerImageBtnClick = () => {
+ open(DeleteDialog);
+ };
+ const [isHovered, setIsHovered] = useState(false);
+
+ return (
+
+
+ 타이머 배경
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ className="tooptip-button"
+ />
+ {isHovered && }
+
+
+
+
+ {selectedTheme.useTimerUrl && (
+
+
+
+ )}
+
+
+ {selectedTheme.useTimerUrl ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/app/admin/(consts)/sidebar.ts b/app/admin/(consts)/sidebar.ts
index e92545f..5a85af4 100644
--- a/app/admin/(consts)/sidebar.ts
+++ b/app/admin/(consts)/sidebar.ts
@@ -43,20 +43,6 @@ export const smallXProps = {
height: 16,
};
-export const previewProps = {
- src: "/images/svg/image.png",
- alt: "x icon",
- width: 315,
- height: 682,
-};
-
-export const statusBarProps = {
- src: "/images/svg/status_bar.svg",
- alt: "status_bar",
- width: 315,
- height: 40,
-};
-
export const timerPreviewProps = {
src: "/images/svg/timer_preview.svg",
alt: "timer_preview",
@@ -71,6 +57,22 @@ export const settingProps = {
height: 24,
};
+export const QuestionIconProps = {
+ src: "/images/svg/icon_question.svg",
+ alt: "gallery_image",
+ width: 24,
+ height: 24,
+};
+
+export const timerPreviewLineProps = {
+ src: "/images/svg/timer_preview_entire.svg",
+ alt: "TIMER_LINE_IMAGE",
+ width: 158,
+ height: 340,
+};
+
+export const defaultTimerImage = "/images/svg/icon_preview.svg";
+export const defaultTimerImagePreview = "/images/svg/timer_preview.svg";
export const timerTooltipProps = {
src: "/images/png/tooltip.png",
alt: "tooltip",
diff --git a/app/admin/(style)/admin.modules.sass b/app/admin/(style)/admin.modules.sass
index 394624b..28218a1 100644
--- a/app/admin/(style)/admin.modules.sass
+++ b/app/admin/(style)/admin.modules.sass
@@ -149,7 +149,6 @@
width: 100vw
height: 100vh
background-color: $color-black60
- z-index: 100
.modal-1
position: fixed
left: 0
diff --git a/app/admin/(style)/themeInfo.modules.sass b/app/admin/(style)/themeInfo.modules.sass
index 2840481..f523f19 100644
--- a/app/admin/(style)/themeInfo.modules.sass
+++ b/app/admin/(style)/themeInfo.modules.sass
@@ -13,7 +13,8 @@
@include title24SB
color: $color-white
position: relative
-
+ div
+ cursor: default
.drawer-open
width: calc(100% - 520px)
@@ -22,7 +23,7 @@
position: absolute
top: 126px
left: -40px
- width: 100vw
+ width: calc(100% + 80px)
height: 1px
background-color: $color-white5
@@ -40,6 +41,7 @@
position: absolute
right: 0
top: 30px
+ cursor: pointer
.theme-infomation-text
@include body14M
@@ -324,6 +326,7 @@
span
@include title16SB
margin-right: 2px
+ cursor: default
img
vertical-align: bottom
diff --git a/app/admin/Admin.tsx b/app/admin/Admin.tsx
index b91483e..88dee7e 100644
--- a/app/admin/Admin.tsx
+++ b/app/admin/Admin.tsx
@@ -21,15 +21,19 @@ type Theme = {
function Admin() {
const { data: categories = [], isLoading } = useGetThemeList();
-
const isLoggedIn = useCheckSignIn();
const [selectedTheme, setSelectedTheme] = useSelectedTheme();
- const { adminCode, shopName } = getLoginInfo();
const [toast, setToast] = useToastInfo();
const router = useRouter();
+ useEffect(() => {
+ if (!isLoading && categories.length > 0 && selectedTheme.id === 0) {
+ setSelectedTheme(categories[categories.length - 1]);
+ }
+ }, [isLoading]);
+
const handleClickSelected = (theme: Theme) => {
setSelectedTheme(theme);
setSelectedThemeId(theme.id);
@@ -49,18 +53,13 @@ function Admin() {
}, [toast, setToast]);
const SidebarViewProps = {
- adminCode,
- shopName,
categories,
selectedTheme,
handleClickSelected,
isOpen: toast.isOpen,
+ isLoading,
};
- if (!isLoggedIn || isLoading) {
- return ;
- }
-
return ;
}
diff --git a/app/admin/AdminView.tsx b/app/admin/AdminView.tsx
index 1e956b1..73a5394 100644
--- a/app/admin/AdminView.tsx
+++ b/app/admin/AdminView.tsx
@@ -7,6 +7,7 @@ import Toast from "@/components/common/Toast/Toast";
import NotiDialog from "@/components/common/Dialog-new/Noti-Dialog-new/Dialog";
import useModal from "@/hooks/useModal";
import { getLocalStorage } from "@/utils/storageUtil";
+import Loader from "@/components/Loader/Loader";
interface Theme {
id: number;
@@ -16,24 +17,25 @@ interface Theme {
}
interface Props {
- adminCode: string;
- shopName: string;
categories: Theme[];
selectedTheme: Theme;
isOpen: boolean;
+ isLoading: boolean;
handleClickSelected: (theme: Theme) => void;
}
function AdminView(props: Props) {
- const { isOpen } = props;
- const { open } = useModal();
+ const { isOpen, isLoading } = props;
+ const { open, closeAll } = useModal();
const isHideDialog = getLocalStorage("hideDialog");
useEffect(() => {
+ closeAll();
if (!isHideDialog) {
open(NotiDialog, { type: "put" });
}
}, []);
+ if (isLoading) return ;
return (
diff --git a/app/components/atoms/selectedTheme.atom.ts b/app/components/atoms/selectedTheme.atom.ts
index 118ed5c..81206a9 100644
--- a/app/components/atoms/selectedTheme.atom.ts
+++ b/app/components/atoms/selectedTheme.atom.ts
@@ -11,6 +11,8 @@ interface SelectedTheme {
title: string;
timeLimit: number;
hintLimit: number;
+ themeImageUrl?: string;
+ useTimerUrl?: boolean;
}
export const InitialSelectedTheme: SelectedTheme = {
diff --git a/app/components/atoms/timerImage.atom.ts b/app/components/atoms/timerImage.atom.ts
new file mode 100644
index 0000000..05a1bd5
--- /dev/null
+++ b/app/components/atoms/timerImage.atom.ts
@@ -0,0 +1,18 @@
+import {
+ atom,
+ useRecoilValue,
+ useRecoilState,
+ useSetRecoilState,
+} from "recoil";
+
+interface TimerImageType {
+ timerImage: File | undefined;
+}
+const timerImage = atom
({
+ key: "timerImage",
+ default: { timerImage: undefined },
+});
+
+export const useTimerImage = () => useRecoilState(timerImage);
+export const useTimerImageValue = () => useRecoilValue(timerImage);
+export const useTimerImageWrite = () => useSetRecoilState(timerImage);
diff --git a/app/components/common/Dialog-new/Image-Dialog-new/Dialog.tsx b/app/components/common/Dialog-new/Image-Dialog-new/Dialog.tsx
index e17845c..a240916 100644
--- a/app/components/common/Dialog-new/Image-Dialog-new/Dialog.tsx
+++ b/app/components/common/Dialog-new/Image-Dialog-new/Dialog.tsx
@@ -1,82 +1,45 @@
-import React, { forwardRef, useRef } from "react";
-import { SubmitHandler, useForm } from "react-hook-form";
+import React, { FormEvent, forwardRef, useRef } from "react";
import Image from "next/image";
-import { usePutTheme } from "@/mutations/putTheme";
-import { useDeleteTheme } from "@/mutations/deleteTheme";
-import {
- useSelectedTheme,
- useSelectedThemeReset,
-} from "@/components/atoms/selectedTheme.atom";
-import {
- useCreateThemeReset,
- useCreateThemeValue,
-} from "@/components/atoms/createTheme.atom";
+import { useSelectedTheme } from "@/components/atoms/selectedTheme.atom";
import useClickOutside from "@/hooks/useClickOutside";
-import { deleteProps, xProps } from "@/admin/(consts)/sidebar";
+import { xProps } from "@/admin/(consts)/sidebar";
import useModal from "@/hooks/useModal";
import ModalPortal from "@/components/common/Dialog-new/ModalPortal";
-
-import DialogBody from "./DialogBody";
-
import "@/components/common/Dialog-new/dialog.sass";
+import useTimerImageUpload from "@/mutations/useTimerImageUpload";
+import { useTimerImageValue } from "@/components/atoms/timerImage.atom";
-interface DialogProps {
- type?: string | "";
-}
-
-interface FormValues {
- id: number;
- title: string;
- timeLimit: number;
- hintLimit: number;
-}
+import DialogBody from "./DialogBody";
-const Dialog = forwardRef((props) => {
- const { open, close } = useModal();
- const { type = "" } = props;
+const Dialog = forwardRef(() => {
+ const { close } = useModal();
const formRef = useRef(null);
- const handleOpenDeleteModal = (event: React.MouseEvent) => {
- event.stopPropagation();
- open(Dialog, { type: "delete" });
- };
-
- const { handleSubmit } = useForm();
const [selectedTheme, setSelectedTheme] = useSelectedTheme();
- const createTheme = useCreateThemeValue();
- const resetCreateTheme = useCreateThemeReset();
- const resetSelectedTheme = useSelectedThemeReset();
- const isDisabled =
- type === "put"
- ? (String(createTheme.title) === String(selectedTheme.title) &&
- Number(createTheme.timeLimit) === Number(selectedTheme.timeLimit) &&
- Number(createTheme.hintLimit) === Number(selectedTheme.hintLimit)) ||
- !(createTheme.title && createTheme.timeLimit && createTheme.hintLimit)
- : !(createTheme.title && createTheme.timeLimit && createTheme.hintLimit);
-
- const { mutateAsync: putTheme } = usePutTheme();
- const { mutateAsync: deleteTheme } = useDeleteTheme();
+ const { timerImage } = useTimerImageValue();
+ const { handleProcess } = useTimerImageUpload();
- const onSubmit: SubmitHandler = () => {
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
const { id } = selectedTheme;
const submitData = {
- ...createTheme,
- id,
+ themeId: id,
+ timerImageFile: timerImage,
};
-
- if (type === "put") {
- putTheme(submitData);
- setSelectedTheme(submitData);
- } else if (type === "delete") {
- deleteTheme({ id });
- resetSelectedTheme();
+ try {
+ const imageUrl = await handleProcess(submitData);
+ setSelectedTheme((prev) => ({
+ ...prev,
+ useTimerUrl: true,
+ themeImageUrl: imageUrl,
+ }));
+ } catch (error) {
+ console.error(error);
}
- close();
- resetCreateTheme();
- return close();
+ close();
};
useClickOutside(formRef, close);
@@ -84,20 +47,22 @@ const Dialog = forwardRef((props) => {
return (