diff --git a/src/app/globals.css b/src/app/globals.css
index 0492d713..c72a75b2 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -56,3 +56,12 @@ input:-webkit-autofill:active {
width: 8px;
height: 8px;
}
+
+button {
+ cursor: pointer;
+}
+
+button:hover,
+button:active {
+ filter: brightness(0.92);
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 1ce61d4c..4c22ac0b 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -7,7 +7,7 @@ import "@mantine/notifications/styles.css";
import "./globals.css";
import { ColorSchemeScript, MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications";
-import { GoogleAnalytics } from "@next/third-parties/google";
+import { GoogleAnalytics, GoogleTagManager } from "@next/third-parties/google";
import { METADATA } from "@/constants/metadata";
const pretendard = localFont({
@@ -52,6 +52,9 @@ export default function RootLayout({
className={pretendard.className}
style={{ overflowX: "hidden", overflowY: "hidden" }}
>
+
diff --git a/src/app/robots.txt b/src/app/robots.txt
new file mode 100644
index 00000000..d7e6ce7b
--- /dev/null
+++ b/src/app/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /photographer/, /customer/
\ No newline at end of file
diff --git a/src/app/sitemap.xml b/src/app/sitemap.xml
new file mode 100644
index 00000000..e63b8a03
--- /dev/null
+++ b/src/app/sitemap.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ https://www.freebe.co.kr/
+ 2024-11-05T07:25:39+00:00
+
+
+
+
\ No newline at end of file
diff --git a/src/components/buttons/buttons.css.ts b/src/components/buttons/buttons.css.ts
index bd184008..2d081682 100644
--- a/src/components/buttons/buttons.css.ts
+++ b/src/components/buttons/buttons.css.ts
@@ -1,7 +1,7 @@
import { breakpoints } from "@/styles/breakpoints.css";
import sprinkles from "@/styles/sprinkles.css";
import { texts } from "@/styles/text.css";
-import { style, styleVariants } from "@vanilla-extract/css";
+import { keyframes, style, styleVariants } from "@vanilla-extract/css";
const baseTab = style([
sprinkles({ borderColor: "stroke-grey" }),
@@ -51,6 +51,19 @@ const baseBottomButton = style([
},
]);
+const rotateAnimation = keyframes({
+ from: {
+ transform: "rotate(0deg)",
+ },
+ to: {
+ transform: "rotate(360deg)",
+ },
+});
+
+export const loaderStyle = style({
+ animation: `${rotateAnimation} 2s linear infinite`,
+});
+
const buttonStyles = styleVariants({
kakao: {
display: "flex",
@@ -68,9 +81,7 @@ const buttonStyles = styleVariants({
fontSize: 17,
fontWeight: 600,
color: "#1a1a1a",
- ":hover": {
- cursor: "pointer",
- },
+ cursor: "pointer",
},
add: [
texts["button-01"],
@@ -145,6 +156,12 @@ const buttonStyles = styleVariants({
{
border: "none",
cursor: "initial",
+ ":hover": {
+ filter: "none",
+ },
+ ":active": {
+ filter: "none",
+ },
},
],
primary: [
@@ -174,10 +191,16 @@ const buttonStyles = styleVariants({
color: "blue",
}),
{
- background: "none",
+ backgroundColor: "transparent",
borderStyle: "solid",
borderRadius: 8,
borderWidth: 1,
+ ":hover": {
+ backgroundColor: "white",
+ },
+ ":active": {
+ backgroundColor: "white",
+ },
},
],
secondary: [
@@ -203,6 +226,12 @@ const buttonStyles = styleVariants({
{
border: "none",
cursor: "initial",
+ ":hover": {
+ filter: "none",
+ },
+ ":active": {
+ filter: "none",
+ },
},
],
linkArea: {
@@ -220,11 +249,20 @@ export const closeStyles = styleVariants({
zIndex: 3,
display: "flex",
alignItems: "center",
+ cursor: "pointer",
},
grey: [
sprinkles({
color: "stroke-grey",
}),
+ {
+ ":hover": {
+ filter: "brightness(0.7);",
+ },
+ ":active": {
+ filter: "brightness(0.7);",
+ },
+ },
],
white: [
sprinkles({
diff --git a/src/components/buttons/common-buttons.tsx b/src/components/buttons/common-buttons.tsx
index ce180b2d..9ef6d6b2 100644
--- a/src/components/buttons/common-buttons.tsx
+++ b/src/components/buttons/common-buttons.tsx
@@ -3,9 +3,10 @@ import {
ButtonHTMLAttributes,
MouseEventHandler,
} from "react";
+import Image from "next/image";
import Link from "next/link";
import { LinkType } from "profile-types";
-import buttonStyles from "./buttons.css";
+import buttonStyles, { loaderStyle } from "./buttons.css";
interface ButtonProps
extends DetailedHTMLProps<
@@ -20,6 +21,7 @@ interface ButtonOptions {
size: "xs" | "sm" | "md" | "lg";
styleType: "primary" | "secondary" | "line" | "danger";
link?: string;
+ loading?: boolean;
}
export const CustomButton = ({
@@ -30,18 +32,29 @@ export const CustomButton = ({
disabled,
children,
link,
+ loading,
...props
}: ButtonProps & ButtonOptions) => {
return (
);
@@ -125,16 +138,28 @@ export const BottomButton = ({
onClick,
disabled,
type,
+ loading,
...props
-}: ButtonProps) => {
+}: ButtonProps & { loading?: boolean }) => {
return (
);
};
diff --git a/src/components/common/bottom-sheet.tsx b/src/components/common/bottom-sheet.tsx
index 92df5c04..68673bcf 100644
--- a/src/components/common/bottom-sheet.tsx
+++ b/src/components/common/bottom-sheet.tsx
@@ -1,4 +1,5 @@
import { useState } from "react";
+import Image from "next/image";
import { bottomSheetStyles } from "./common.css";
const BottomSheet = ({ children }: { children: React.ReactNode }) => {
@@ -25,13 +26,22 @@ const BottomSheet = ({ children }: { children: React.ReactNode }) => {
}
}
>
-
diff --git a/src/components/common/common.css.ts b/src/components/common/common.css.ts
index 212e6259..c238d70c 100644
--- a/src/components/common/common.css.ts
+++ b/src/components/common/common.css.ts
@@ -1,6 +1,6 @@
import sprinkles from "@/styles/sprinkles.css";
import { texts } from "@/styles/text.css";
-import { style, styleVariants } from "@vanilla-extract/css";
+import { keyframes, style, styleVariants } from "@vanilla-extract/css";
export const captionStyle = style([
texts["caption-01"],
@@ -15,6 +15,24 @@ export const captionStyle = style([
},
]);
+const rotateAnimation = keyframes({
+ from: {
+ transform: "rotate(0deg)",
+ },
+ to: {
+ transform: "rotate(360deg)",
+ },
+});
+
+export const loaderStyle = style({
+ animation: `${rotateAnimation} 2s linear infinite`,
+});
+
+const baseIcon = style({
+ transition: "transform 0.5s ease",
+ paddingLeft: 3,
+});
+
export const bottomSheetStyles = styleVariants({
container: {
display: "flex",
@@ -51,6 +69,24 @@ export const bottomSheetStyles = styleVariants({
position: "relative",
height: "100%",
},
+ control: [
+ sprinkles({ borderColor: "stroke-grey", backgroundColor: "white" }),
+ {
+ borderWidth: 1,
+ borderStyle: "solid",
+ borderRadius: "100%",
+ position: "absolute",
+ top: -10,
+ left: "calc(50% - 19px)",
+ width: 38,
+ height: 38,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ ],
+ openIcon: [baseIcon, { transform: "rotate(270deg)" }],
+ closeIcon: [baseIcon, { transform: "rotate(90deg)" }],
});
export const profileStyles = styleVariants({
@@ -271,12 +307,10 @@ export const chipStyles = styleVariants({
highlight: [
commonChipStyle,
sprinkles({
+ backgroundColor: "white",
color: "blue",
borderColor: "blue",
}),
- {
- background: "none",
- },
],
selectedContainer: [
sprinkles({
diff --git a/src/components/common/loader.tsx b/src/components/common/loader.tsx
new file mode 100644
index 00000000..c127f3b4
--- /dev/null
+++ b/src/components/common/loader.tsx
@@ -0,0 +1,16 @@
+import Image from "next/image";
+import { loaderStyle } from "./common.css";
+
+const Loader = () => {
+ return (
+
+ );
+};
+
+export default Loader;
diff --git a/src/components/images/images.css.ts b/src/components/images/images.css.ts
index d8608525..d6f104ea 100644
--- a/src/components/images/images.css.ts
+++ b/src/components/images/images.css.ts
@@ -55,6 +55,12 @@ export const GridStyles = styleVariants({
outlineOffset: -1,
outlineColor: BLUE01,
boxSizing: "content-box",
+ ":hover": {
+ filter: "brightness(0.92);",
+ },
+ ":active": {
+ filter: "brightness(0.92);",
+ },
},
});
diff --git a/src/containers/common/image-stack/image-stack.css.ts b/src/containers/common/image-stack/image-stack.css.ts
index fe39e8ff..f3f92691 100644
--- a/src/containers/common/image-stack/image-stack.css.ts
+++ b/src/containers/common/image-stack/image-stack.css.ts
@@ -30,6 +30,9 @@ const navButtonStyle = style([
":hover": {
animation: `${navButtonFrames} 1s ease-in-out infinite`,
},
+ ":active": {
+ animation: `${navButtonFrames} 1s ease-in-out infinite`,
+ },
"@media": {
[breakpoints.mobile]: {
aspectRatio: "1",
@@ -100,6 +103,12 @@ export const imageStackStyles = styleVariants({
buttonWrapper: {
border: "none",
background: "none",
+ ":hover": {
+ filter: "none",
+ },
+ ":active": {
+ filter: "none",
+ },
},
prev: [navButtonStyle, { left: "-50px" }],
next: [navButtonStyle, { right: "-50px" }],
diff --git a/src/containers/common/notice-banner/index.tsx b/src/containers/common/notice-banner/index.tsx
index 53e8a121..10c63781 100644
--- a/src/containers/common/notice-banner/index.tsx
+++ b/src/containers/common/notice-banner/index.tsx
@@ -4,35 +4,38 @@ import { carouselStyles } from "@/styles/mantine.css";
import { Carousel } from "@mantine/carousel";
import Image from "next/image";
import Link from "next/link";
-import { CSSProperties } from "react";
+import { CSSProperties, useEffect, useState } from "react";
import {
customedCarouselStyles,
noticeBannerStyles,
} from "./notice-banner.css";
+type BannerTargets = "join" | "main";
+
+type Banner = {
+ image: string;
+ link?: string;
+} & {
+ [key in BannerTargets]: boolean;
+};
+
const NoticeBanner = ({
container,
target,
}: {
container?: CSSProperties;
- target: "join" | "main";
+ target: BannerTargets;
}) => {
- const bannerDatas = {
- join: [
- {
- image:
- "https://local-freebe-data.s3.ap-northeast-2.amazonaws.com/service-banner/join-banner.png",
- link: undefined,
- },
- ],
- main: [
- {
- image:
- "https://local-freebe-data.s3.ap-northeast-2.amazonaws.com/service-banner/main-banner.png",
- link: "https://slashpage.com/freebe/xjqy1g2vqrk4dm6vd54z",
- },
- ],
- };
+ const [bannerDatas, setBannerDatas] = useState
([]);
+
+ useEffect(() => {
+ const dataLayer = (window as any).dataLayer || [];
+ const bannerData = dataLayer.find(
+ (data: any) => data.event === "loadBannerData",
+ )?.bannerData;
+ if (bannerData) setBannerDatas(bannerData);
+ }, []);
+
return (
- {bannerDatas[target].map((banner, index) => (
-
- {banner.link ? (
-
+ {bannerDatas
+ .filter((data) => data[target])
+ .map((banner, index) => (
+
+ {banner.link ? (
+
+
+
+ ) : (
-
- ) : (
-
- )}
-
- ))}
+ )}
+
+ ))}
);
};
diff --git a/src/containers/common/notice-banner/notice-banner.css.ts b/src/containers/common/notice-banner/notice-banner.css.ts
index bdf72424..a160aade 100644
--- a/src/containers/common/notice-banner/notice-banner.css.ts
+++ b/src/containers/common/notice-banner/notice-banner.css.ts
@@ -6,7 +6,7 @@ export const customedCarouselStyles = styleVariants({
position: "relative",
width: "100%",
minWidth: 700,
- maxWidth: 950,
+ maxWidth: 800,
paddingBottom: 30,
"@media": {
diff --git a/src/containers/customer/products/info.tsx b/src/containers/customer/products/info.tsx
index 4bccd8e2..259f283e 100644
--- a/src/containers/customer/products/info.tsx
+++ b/src/containers/customer/products/info.tsx
@@ -3,6 +3,7 @@
import Image from "next/image";
import { Product } from "product-types";
import { PageParams } from "route-parameters";
+import { sendGAEvent } from "@next/third-parties/google";
import { Carousel } from "@mantine/carousel";
import { useDisclosure } from "@mantine/hooks";
import { Modal, Tabs } from "@mantine/core";
@@ -34,6 +35,14 @@ const ProductInfo = ({
}: Product & Pick) => {
const [opened, { open, close }] = useDisclosure(false);
+ function handleStartReservation() {
+ sendGAEvent("event", "start_reservation", {
+ profile_name: profileName,
+ product_id: productId,
+ });
+ open();
+ }
+
return (
{
return (
-
+
);
})}
@@ -101,7 +116,7 @@ const ProductInfo = ({
diff --git a/src/containers/customer/reservation/details/cancel-modal.tsx b/src/containers/customer/reservation/details/cancel-modal.tsx
index 017fd0ca..984ff084 100644
--- a/src/containers/customer/reservation/details/cancel-modal.tsx
+++ b/src/containers/customer/reservation/details/cancel-modal.tsx
@@ -22,8 +22,10 @@ const CancelModal = ({
const router = useRouter();
const { formId } = useParams>();
const [cancellationReason, setCancellationReason] = useState("");
+ const [loadingCancel, setLoadingCancel] = useState(false);
async function handleCancel() {
+ setLoadingCancel(true);
await responseHandler(
cancelReservation(
parseInt(formId, PARAMETER_DEFAULT_RADIX),
@@ -39,6 +41,7 @@ const CancelModal = ({
router.refresh();
},
);
+ setLoadingCancel(false);
}
return (
@@ -64,6 +67,7 @@ const CancelModal = ({
styleType="primary"
title="취소하기"
disabled={cancellationReason === ""}
+ loading={loadingCancel}
onClick={handleCancel}
/>
diff --git a/src/containers/customer/reservation/reference-form.tsx b/src/containers/customer/reservation/reference-form.tsx
index d53596bd..ce8c3520 100644
--- a/src/containers/customer/reservation/reference-form.tsx
+++ b/src/containers/customer/reservation/reference-form.tsx
@@ -3,6 +3,7 @@
import { usePathname, useRouter } from "next/navigation";
import { useFormContext } from "react-hook-form";
import { reservation } from "product-types";
+import { sendGAEvent } from "@next/third-parties/google";
import { BottomButton } from "@/components/buttons/common-buttons";
import popToast from "@/components/common/toast";
import ReferenceGrid from "@/containers/customer/reservation/reference/grid";
@@ -17,7 +18,11 @@ const ReferenceForm = ({ images }: { images: string[] }) => {
const { setValue, watch } = useFormContext();
const currentPath = usePathname();
- const selectedImageList = watch("referenceImages");
+ const [selectedImageList, productId, profileName] = watch([
+ "referenceImages",
+ "productId",
+ "profileName",
+ ]);
const addImage = (url: string, file?: File) => {
const currentImages = selectedImageList || [];
@@ -49,6 +54,10 @@ const ReferenceForm = ({ images }: { images: string[] }) => {
function handleNext() {
const nextPath = getNextPath("submit");
+ sendGAEvent("event", "add_reference", {
+ profile_name: profileName,
+ product_id: productId,
+ });
router.push(nextPath);
}
diff --git a/src/containers/customer/reservation/reference/grid/grid.css.ts b/src/containers/customer/reservation/reference/grid/grid.css.ts
index 42a3c8f5..af571673 100644
--- a/src/containers/customer/reservation/reference/grid/grid.css.ts
+++ b/src/containers/customer/reservation/reference/grid/grid.css.ts
@@ -4,7 +4,7 @@ import { styleVariants } from "@vanilla-extract/css";
export const fileSelectStyles = styleVariants({
container: [
- sprinkles({ borderColor: "blue" }),
+ sprinkles({ borderColor: "blue", backgroundColor: "white" }),
{
width: "100%",
position: "relative",
@@ -15,6 +15,12 @@ export const fileSelectStyles = styleVariants({
paddingTop: 8,
paddingBottom: 8,
marginBottom: 20,
+ ":hover": {
+ filter: "brightness(0.92);",
+ },
+ ":active": {
+ filter: "brightness(0.92);",
+ },
},
],
info: [
diff --git a/src/containers/customer/reservation/reservation-form-provider.tsx b/src/containers/customer/reservation/reservation-form-provider.tsx
index ad9328b8..6a395695 100644
--- a/src/containers/customer/reservation/reservation-form-provider.tsx
+++ b/src/containers/customer/reservation/reservation-form-provider.tsx
@@ -6,6 +6,7 @@ import { ID_REGEX } from "@/constants/common/user";
import { postReservation } from "@/services/client/customer/reservation";
import { responseHandler } from "@/services/common/error";
import { zodResolver } from "@hookform/resolvers/zod";
+import { sendGAEvent } from "@next/third-parties/google";
import { useRouter } from "next/navigation";
import { reservation } from "product-types";
import { FormProvider, useForm } from "react-hook-form";
@@ -87,6 +88,10 @@ const ReservationFormProvider = ({
const { handleSubmit } = method;
async function onSubmit(data: reservation.FormType) {
+ sendGAEvent("event", "submit_reservation", {
+ profile_name: data.profileName,
+ product_id: data.productId,
+ });
await responseHandler(
postReservation(data),
(formId) => {
diff --git a/src/containers/customer/reservation/submit-form.tsx b/src/containers/customer/reservation/submit-form.tsx
index 6452eae9..70a7b3a8 100644
--- a/src/containers/customer/reservation/submit-form.tsx
+++ b/src/containers/customer/reservation/submit-form.tsx
@@ -33,7 +33,7 @@ const SubmitForm = ({
const {
setValue,
watch,
- formState: { touchedFields },
+ formState: { touchedFields, isSubmitting },
} = useFormContext();
const [totalPrice, noticeAgreement] = watch([
"totalPrice",
@@ -80,6 +80,7 @@ const SubmitForm = ({
title="신청하기"
type="submit"
disabled={!noticeAgreement}
+ loading={isSubmitting}
/>
);
diff --git a/src/containers/photographer/join/index.tsx b/src/containers/photographer/join/index.tsx
index 456a6ede..3d4613bb 100644
--- a/src/containers/photographer/join/index.tsx
+++ b/src/containers/photographer/join/index.tsx
@@ -4,6 +4,7 @@ import { useRouter } from "next/navigation";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import { Join } from "profile-types";
+import { sendGAEvent } from "@next/third-parties/google";
import { zodResolver } from "@hookform/resolvers/zod";
import { MAX_LENGTHS } from "@/constants/common/schema";
import { ID_REGEX } from "@/constants/common/user";
@@ -15,6 +16,16 @@ import { CUSTOMED_CODE, responseHandler } from "@/services/common/error";
import Profile from "./profile";
import { joinStyles } from "./join.css";
+const RESERVED_PROFILE_NAMES = [
+ ".env",
+ "photographer",
+ "customer",
+ "api",
+ "auth",
+ "login",
+];
+const IS_RESERVED_PROFILE_NAME = "사용할 수 없는 아이디입니다.";
+
const PhotographerJoin = () => {
const router = useRouter();
@@ -39,10 +50,17 @@ const PhotographerJoin = () => {
defaultValues,
resolver: zodResolver(joinSchema),
});
- const { handleSubmit, setError } = method;
+ const {
+ handleSubmit,
+ setError,
+ formState: { isSubmitting },
+ } = method;
function handleSubmitFail(message?: string) {
- if (message === CUSTOMED_CODE.PROFILE_NAME_ALREADY_EXISTS) {
+ if (
+ message === CUSTOMED_CODE.PROFILE_NAME_ALREADY_EXISTS ||
+ message === IS_RESERVED_PROFILE_NAME
+ ) {
setError(
"profileName",
{ message: "아이디를 다시 설정해주세요." },
@@ -52,15 +70,24 @@ const PhotographerJoin = () => {
popToast("다시 시도해 주세요.", message || "가입에 실패했습니다.", true);
}
+ function isReservedProfileName(profileName: string) {
+ return RESERVED_PROFILE_NAMES.includes(profileName);
+ }
+
async function onSubmit(data: Join) {
- await responseHandler(
- postProfile(data),
- (url) => {
- popToast("가입이 완료되었습니다!");
- router.push(`/photographer?url=${url}&tutorial=true`);
- },
- handleSubmitFail,
- );
+ if (isReservedProfileName(data.profileName)) {
+ handleSubmitFail(IS_RESERVED_PROFILE_NAME);
+ } else {
+ sendGAEvent("event", "enroll", { profile_name: data.profileName });
+ await responseHandler(
+ postProfile(data),
+ (url) => {
+ popToast("가입이 완료되었습니다!");
+ router.push(`/photographer?url=${url}&tutorial=true`);
+ },
+ handleSubmitFail,
+ );
+ }
}
return (
@@ -76,6 +103,7 @@ const PhotographerJoin = () => {
size="md"
styleType="primary"
title="가입하기"
+ loading={isSubmitting}
style={{ marginTop: 30 }}
/>
diff --git a/src/containers/photographer/main/header.tsx b/src/containers/photographer/main/header.tsx
index 2b6cb8d5..cb38cc9f 100644
--- a/src/containers/photographer/main/header.tsx
+++ b/src/containers/photographer/main/header.tsx
@@ -1,11 +1,11 @@
"use client";
import { useEffect } from "react";
-import { useRouter, useSearchParams } from "next/navigation";
+import { usePathname, useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import { LinkType } from "profile-types";
-import { Drawer, Menu } from "@mantine/core";
+import { Drawer, FocusTrap, Menu } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { SERVICE_LINKS } from "@/constants/common/common";
import CloseButton from "@/components/buttons/close-button";
@@ -23,6 +23,7 @@ const Header = ({
links?: LinkType[];
}) => {
const router = useRouter();
+ const pathName = usePathname();
const searchParams = useSearchParams();
const [opened, { close, open }] = useDisclosure(false);
const [isOnTutorial, { close: closeTutorial, open: openTutorial }] =
@@ -36,9 +37,15 @@ const Header = ({
const tutorialParam = searchParams.get("tutorial");
if (tutorialParam) {
openTutorial();
+ } else {
+ closeTutorial();
}
}, [searchParams]);
+ useEffect(() => {
+ close();
+ }, [pathName]);
+
return (