diff --git a/.env b/.env deleted file mode 100644 index b4f36c72..00000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -NEXT_PUBLIC_API_URL: "http://43.201.21.97:3002" diff --git a/src/app/(intro)/page.tsx b/src/app/(intro)/page.tsx deleted file mode 100644 index 84170929..00000000 --- a/src/app/(intro)/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; -import { ClientRoute } from "@config/route"; - -export default function Intro() { - return ( -
- Next.js logo - - 시작하기 - - - 관리자 - -
- ); -} diff --git a/src/app/admin/block/components/buttons/add-button.tsx b/src/app/admin/block/components/buttons/add-button.tsx index 4b3b878f..e7bfb5c3 100644 --- a/src/app/admin/block/components/buttons/add-button.tsx +++ b/src/app/admin/block/components/buttons/add-button.tsx @@ -1,18 +1,17 @@ import React from "react"; -interface Props { +type Props = { text: string; - onClick: () => void; - disabled?: boolean; -} -const AddButton = ({ text, onClick, disabled }: Props) => { - const bgColor = disabled ? "bg-orange-100" : "bg-orange-600"; +} & React.ComponentPropsWithoutRef<"button">; +const AddButton = ({ text, ...buttonProps }: Props) => { + const { disabled } = buttonProps; + const bgColor = disabled ? "bg-primary-100" : "bg-primary-450"; const textColor = disabled ? "text-orange-300" : "text-slate-200"; + return ( diff --git a/src/app/admin/block/components/form-input.tsx b/src/app/admin/block/components/form-input.tsx new file mode 100644 index 00000000..e9c9b18e --- /dev/null +++ b/src/app/admin/block/components/form-input.tsx @@ -0,0 +1,33 @@ +"use client"; + +import React from "react"; + +type FormInputProps = { + label: string; + id: string; +} & React.ComponentPropsWithoutRef<"input">; + +export default function FormInput({ + label, + id, + ...inputProps +}: FormInputProps) { + const { required, maxLength, value } = inputProps; + return ( +
+
+ + {maxLength && ( +
+ {value ? (value as string)?.length : 0} + / {maxLength} +
+ )} +
+ +
+ ); +} diff --git a/src/app/admin/block/components/layout.tsx b/src/app/admin/block/components/layout.tsx index 31aaa855..aae284f2 100644 --- a/src/app/admin/block/components/layout.tsx +++ b/src/app/admin/block/components/layout.tsx @@ -1,28 +1,38 @@ "use client"; -import React from "react"; +import React, { FormEvent } from "react"; import Image from "next/image"; import { useRouter } from "next/navigation"; +import QuestionIcon from "@app/admin/block/components/question-icon"; const Layout = ({ title, + onSubmit, children, }: Readonly<{ title: string; + onSubmit?: (e: FormEvent) => void; children: React.ReactNode; }>) => { const router = useRouter(); return ( -
- -

{title}

-
{children}
+
+
+ +
+
+

{title}

+ +
+
+ {children} +
); }; diff --git a/src/app/admin/block/components/question-icon.tsx b/src/app/admin/block/components/question-icon.tsx new file mode 100644 index 00000000..a50a2f40 --- /dev/null +++ b/src/app/admin/block/components/question-icon.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import Image from "next/image"; + +interface Props { + title: string; +} +const QuestionIcon = ({ title }: Props) => { + if (title !== "링크 블록") return; + return ( +
+ question +
+ • 기본 정보와 공개 여부 값은 필수입니다.
• 예약 공개와 스티커는 + 프로 기능입니다. +
+
+
+ ); +}; + +export default QuestionIcon; diff --git a/src/app/admin/block/components/text-input-box.tsx b/src/app/admin/block/components/text-input-box.tsx index 85ef71f1..4f426985 100644 --- a/src/app/admin/block/components/text-input-box.tsx +++ b/src/app/admin/block/components/text-input-box.tsx @@ -4,7 +4,7 @@ interface Props { title: string; placeholder: string; text: string; - setText: React.Dispatch>; + setText: (text: string) => void; required?: boolean; limit?: number; } diff --git a/src/app/admin/block/event/components/calendar.tsx b/src/app/admin/block/event/components/calendar.tsx index e3f1c45b..759a6fd4 100644 --- a/src/app/admin/block/event/components/calendar.tsx +++ b/src/app/admin/block/event/components/calendar.tsx @@ -1,10 +1,76 @@ -export default function Calendar({ label }: { label: string }) { +import DatePicker from "react-datepicker"; + +export default function Calendar({ + startDate, + setStartDate, + endDate, + setEndDate, + startTime, + setStartTime, + endTime, + setEndTime, +}: { + startDate: Date | null; + setStartDate: (date: Date | null) => void; + endDate: Date | null; + setEndDate: (date: Date | null) => void; + startTime: Date | null; + setStartTime: (date: Date | null) => void; + endTime: Date | null; + setEndTime: (date: Date | null) => void; +}) { return ( - <> - - - - {/* 캘린더 */} - +
+ +
+ {/* 시작 날짜 및 시간 선택 */} +
+ + setStartDate(date)} + dateFormat="yyyy.MM.dd" + placeholderText="날짜 선택" + className="w-full rounded-lg border-2 p-2" + /> + setStartTime(date)} + showTimeSelect + showTimeSelectOnly + timeIntervals={15} + timeCaption="시간 선택" + dateFormat="HH:mm" + placeholderText="시간 선택" + className="w-full rounded-lg border-2 p-2" + /> +
+ {/* 종료 날짜 및 시간 선택 */} +
+ + setEndDate(date)} + dateFormat="yyyy.MM.dd" + placeholderText="날짜 선택" + minDate={startDate || undefined} + className="w-full rounded-lg border-2 p-2" + /> + setEndTime(date)} + showTimeSelect + showTimeSelectOnly + timeIntervals={15} + timeCaption="시간 선택" + dateFormat="HH:mm" + placeholderText="시간 선택" + className="w-full rounded-lg border-2 p-2" + /> +
+
+
); } diff --git a/src/app/admin/block/event/components/event-form.tsx b/src/app/admin/block/event/components/event-form.tsx index 0b691050..9e37b114 100644 --- a/src/app/admin/block/event/components/event-form.tsx +++ b/src/app/admin/block/event/components/event-form.tsx @@ -1,10 +1,14 @@ "use client"; -import { useState } from "react"; -import EventFormInput from "./event-form-input"; +import { FormEvent, useState } from "react"; import Calendar from "./calendar"; import EventPreview from "./event-preview"; -import DatePicker from "react-datepicker"; +import Layout from "../../components/layout"; +import ButtonBox from "../../components/buttons/button-box"; +import AddButton from "../../components/buttons/add-button"; +import FormInput from "../../components/form-input"; +import { useRouter } from "next/navigation"; +import { getSequence } from "lib/get-sequence"; import "react-datepicker/dist/react-datepicker.css"; export default function EventForm() { @@ -16,8 +20,83 @@ export default function EventForm() { const [startTime, setStartTime] = useState(null); const [endTime, setEndTime] = useState(null); + const router = useRouter(); + + function combineDateAndTime(date: Date | null, time: Date | null) { + if (!date || !time) return null; + const combined = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + time.getHours(), + time.getMinutes(), + time.getSeconds(), + ); + return combined.toISOString(); + } + + async function postEvent() { + const token = sessionStorage.getItem("token"); + if (!token) throw new Error("인증 토큰이 없습니다. 다시 로그인해주세요."); + const prevSequence = await getSequence(token); + + const dateStart = combineDateAndTime(startDate, startTime); + const dateEnd = combineDateAndTime(endDate, endTime); + + const postData = { + type: 5, + sequence: prevSequence + 1, + title, // 타이틀 + subText01: description, // 서브타이틀 + subText02: eventGuide, // 가이드라인 + dateStart, // 시작일자 + dateEnd, + }; + + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/link/add`, + { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(postData), + }, + ); + + if (!response.ok) { + const errorResponse = await response.json(); + throw new Error( + `Error: ${response.status}, Message: ${errorResponse.message || "Unknown error"}`, + ); + } + + alert("이벤트 블록이 성공적으로 추가되었습니다🥰"); + router.push("/admin"); + + const responseData = await response.json(); + console.log(responseData); + } catch (error) { + throw new Error( + error instanceof Error ? error.message : "An error occurred", + ); + } + } + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + postEvent(); + setTitle(""); + }; + + const summitButtonDisabled = + !title || !startDate || !endDate || !startTime || !endTime; + return ( - <> + -
+
-
- + setTitle(e.target.value)} required + maxLength={30} /> - setDescription(e.target.value)} - required /> - setEventGuide(e.target.value)} - required /> -
- -
- {/* 시작 날짜 및 시간 선택 */} -
- - setStartDate(date)} - dateFormat="yyyy.MM.dd" - placeholderText="날짜 선택" - className="mb-2 w-full rounded border p-2" - /> - setStartTime(date)} - showTimeSelect - showTimeSelectOnly - timeIntervals={15} - timeCaption="시간 선택" - dateFormat="HH:mm" - placeholderText="시간 선택" - className="w-full rounded border p-2" - /> -
- {/* 종료 날짜 및 시간 선택 */} -
- - setEndDate(date)} - dateFormat="yyyy.MM.dd" - placeholderText="날짜 선택" - minDate={startDate || undefined} - className="mb-2 w-full rounded border p-2" - /> - setEndTime(date)} - showTimeSelect - showTimeSelectOnly - timeIntervals={15} - timeCaption="시간 선택" - dateFormat="HH:mm" - placeholderText="시간 선택" - className="w-full rounded border p-2" - /> -
-
-
- - - - + + + + + +
+ ); } diff --git a/src/app/admin/block/event/page.tsx b/src/app/admin/block/event/page.tsx index 1a016877..87f7f888 100644 --- a/src/app/admin/block/event/page.tsx +++ b/src/app/admin/block/event/page.tsx @@ -5,20 +5,7 @@ import EventForm from "./components/event-form"; export default function page() { return ( <> -
- - 뒤로가기 - - -

이벤트 블록

- - -
+ ); } diff --git a/src/app/admin/block/image/components/image-box.tsx b/src/app/admin/block/image/components/image-box.tsx index fb62d877..539ae4de 100644 --- a/src/app/admin/block/image/components/image-box.tsx +++ b/src/app/admin/block/image/components/image-box.tsx @@ -1,31 +1,31 @@ import React from "react"; import Image from "next/image"; -import ErrorBoundary from "@app/(intro)/components/error-boundary"; +import ErrorBoundary from "@app/intro/components/error-boundary"; interface Props { - handeInputImageClick: () => void; + // handeInputImageClick: () => void; selectedImageUrl: string; } -const ImageBox = ({ handeInputImageClick, selectedImageUrl }: Props) => { +const ImageBox = ({ selectedImageUrl }: Props) => { return ( -
- +
+ {/**/} + {/* */} + {/**/} @@ -37,7 +37,7 @@ const ImageBox = ({ handeInputImageClick, selectedImageUrl }: Props) => { ? selectedImageUrl : "/assets/images/image_block_default.png" } - alt="기본이미지 혹은 선택한 이미지" + alt="이미지 URL을 확인해주세요" width={610} height={610} /> diff --git a/src/app/admin/block/image/page.tsx b/src/app/admin/block/image/page.tsx index 635e0e60..661aba47 100644 --- a/src/app/admin/block/image/page.tsx +++ b/src/app/admin/block/image/page.tsx @@ -1,106 +1,159 @@ "use client"; -import React, { useEffect, useRef, useState } from "react"; +import React, { FormEvent, useEffect, useRef, useState } from "react"; import Layout from "@app/admin/block/components/layout"; import TextInputBox from "@app/admin/block/components/text-input-box"; import Image from "next/image"; import AddButton from "@app/admin/block/components/buttons/add-button"; import ButtonBox from "@app/admin/block/components/buttons/button-box"; -import ErrorBoundary from "@app/(intro)/components/error-boundary"; +import ErrorBoundary from "@app/intro/components/error-boundary"; import ImageBox from "@app/admin/block/image/components/image-box"; import BoundaryImageBox from "@app/admin/block/image/components/image-box"; +import { useRouter } from "next/navigation"; +import { getSequence } from "../../../../lib/get-sequence"; +import FormInput from "@app/admin/block/components/form-input"; +import { checkUrl } from "../../../../lib/check-url"; +import { postBlock } from "../../../../lib/post-block"; const Page = () => { - const inputImageRef = useRef(null); - const [imageUrl, setImageUrl] = useState(""); - const [previewImageUrl, setPreviewImageUrl] = useState(""); + // const inputImageRef = useRef(null); const [title, setTitle] = useState(""); const [connectingUrl, setConnectingUrl] = useState(""); const [selectedImageUrl, setSelectedImageUrl] = useState(""); + const router = useRouter(); - const params = { - type: 4, - // sequence: number, - title, - url: connectingUrl, - imgUrl: imageUrl, + const addImageBlock = () => { + const params = { + type: 4, + title, + url: connectingUrl, + imgUrl: selectedImageUrl, + }; + postBlock("/api/link/add", params, router).then((res) => { + if (res) console.log(res); + }); }; - useEffect(() => { - setSelectedImageUrl(imageUrl || previewImageUrl); - }, [imageUrl, previewImageUrl]); + // const addImageBlock = async () => { + // const token = sessionStorage.getItem("token"); + // if (!token) { + // router.push("/login"); + // return; + // } + // const nowSequence = await getSequence(token); + // + // try { + // const response = await fetch( + // `${process.env.NEXT_PUBLIC_API_URL}/api/link/add`, + // { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // Authorization: `Bearer ${token}`, + // }, + // body: JSON.stringify(params), + // }, + // ); + // if (response.ok) { + // alert("이미지 블록 추가 완료"); + // router.push("/admin"); + // } else { + // const { status } = response; + // console.log(status); + // if (status === 500) { + // alert("서버 에러"); + // } + // } + // } catch (error) { + // console.log(error); + // } + // }; - const handeInputImageClick = () => { - inputImageRef.current?.click(); + const handleAddButtonClick = (e: FormEvent) => { + e.preventDefault(); + if (!selectedImageUrl) return; + addImageBlock(); }; - const selectFile = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) { - return; - } - const reader = new FileReader(); - reader.onload = (e) => { - const dataUrl = e.target?.result; - if (typeof dataUrl !== "string") { - return; - } - setPreviewImageUrl(dataUrl); - }; - reader.readAsDataURL(file); - }; + // const handeInputImageClick = () => { + // inputImageRef.current?.click(); + // }; - const checkImageUrl = (strUrl: string) => { - const expUrl = /^http[s]?\:\/\//i; - return expUrl.test(strUrl); - }; + // const selectFile = (e: React.ChangeEvent) => { + // const file = e.target.files?.[0]; + // if (!file) { + // return; + // } + // const reader = new FileReader(); + // reader.onload = (e) => { + // const dataUrl = e.target?.result; + // if (typeof dataUrl !== "string") { + // return; + // } + // setPreviewImageUrl(dataUrl); + // }; + // reader.readAsDataURL(file); + // }; - const handleAddButtonClick = () => { - if (!checkImageUrl(imageUrl)) { + // const handleAddButtonClick = () => { + // if (!checkImageUrl(imageUrl)) { + // alert("이미지 URL을 확인해주세요."); + // return; + // } + // setSelectedImageUrl(imageUrl || previewImageUrl); + // }; + + const setImageText = (text: string) => { + if (!checkUrl(text) && text !== "") { alert("이미지 URL을 확인해주세요."); return; } - setSelectedImageUrl(imageUrl || previewImageUrl); + setSelectedImageUrl(text); }; return ( - - + - setImageText(e.currentTarget.value)} + required /> + {/**/} - setTitle(e.currentTarget.value)} + maxLength={30} /> - setConnectingUrl(e.currentTarget.value)} /> diff --git a/src/app/admin/block/link/components/link-form.tsx b/src/app/admin/block/link/components/link-form.tsx index e428551c..502ed1c7 100644 --- a/src/app/admin/block/link/components/link-form.tsx +++ b/src/app/admin/block/link/components/link-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { +import React, { ChangeEvent, FormEvent, useCallback, @@ -9,41 +9,15 @@ import { } from "react"; import StylePreview from "./style-preview"; import StyleType from "./style-type"; -import FormInput from "./form-input"; +import FormInput from "../../components/form-input"; import { getSequence } from "lib/get-sequence"; +import AddButton from "@app/admin/block/components/buttons/add-button"; +import ButtonBox from "@app/admin/block/components/buttons/button-box"; +import Layout from "@app/admin/block/components/layout"; +import { useRouter } from "next/navigation"; const styleItemNames = ["썸네일", "심플", "카드", "배경"]; -async function getToken() { - const loginData = { - userId: "linkle", - password: "1234", - }; - try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/api/login`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(loginData), - }, - ); - - if (!response.ok) { - throw new Error(`Login failed: ${response.status}`); - } - - const result = await response.json(); - if (result.code === 200) { - return result.data.token; - } - } catch (error) { - throw new Error( - error instanceof Error ? error.message : "Login error occurred", - ); - } -} - export default function LinkForm() { const [selectedStyle, setSelectedStyle] = useState("썸네일"); const [title, setTitle] = useState(""); @@ -57,9 +31,11 @@ export default function LinkForm() { (url: string) => /^https?:\/\/.+\..+/.test(url), [], ); + const router = useRouter(); async function postLink() { - const token = await getToken(); + const token = sessionStorage.getItem("token"); + if (!token) throw new Error("인증 토큰이 없습니다. 다시 로그인해주세요."); const prevSequence = await getSequence(token); const postData = { @@ -92,6 +68,9 @@ export default function LinkForm() { ); } + alert("링크 블록이 성공적으로 추가되었습니다🥰"); + router.push("/admin"); + // const responseData = await response.json(); // console.log(responseData); } catch (error) { @@ -133,8 +112,15 @@ export default function LinkForm() { } }; + const summitButtonDisabled = + isLinkUrlError || + isImgUrlError || + isImgUrlConnectionError || + (selectedStyle === "심플" && (!linkUrl || !title)) || + (selectedStyle !== "심플" && (!linkUrl || !title || !linkImg)); + return ( - <> + -
+
{/* 스타일 */}

@@ -164,11 +150,11 @@ export default function LinkForm() {

-
+
{/* Info */} -
-
+
+
{isLinkUrlError && ( -
+
올바른 URL 형식을 입력해주세요
)}
-
+
setTitle(e.target.value)} placeholder="타이틀을 입력해주세요" required + maxLength={30} />
-
+
{isImgUrlError && ( -
+
올바른 URL 형식을 입력해주세요
)} {isImgUrlConnectionError && ( -
+
잘못된 이미지 경로입니다
)}
- -
- - - - +
+ + + + +
+ ); } diff --git a/src/app/admin/block/link/components/style-preview.tsx b/src/app/admin/block/link/components/style-preview.tsx index da827f6b..e2f0a39b 100644 --- a/src/app/admin/block/link/components/style-preview.tsx +++ b/src/app/admin/block/link/components/style-preview.tsx @@ -28,7 +28,7 @@ export default function StylePreview({ setHasImgError(false); setIsImgUrlConnectionError(false); - // 이미지 로드 비동기 처리 + // 이미지 로드 비동기 처리(배경 이미지 일 때) const img = new window.Image(); img.src = linkImg; @@ -37,16 +37,17 @@ export default function StylePreview({ }; img.onerror = () => { - setHasImgError(true); // 이미지 로드 실패 + setHasImgError(true); // 이미지 로드 실패시 메시지 표시 setIsImgUrlConnectionError(true); }; } else if (!isValidUrl(linkImg)) { - // URL이 유효하지 않으면 에러 상태 설정하지 않음 + // URL이 유효하지 않으면 메시지 표시x setHasImgError(false); setIsImgUrlConnectionError(false); } }, [linkImg, selectedStyle, setIsImgUrlConnectionError, isValidUrl]); + // Image에서 error 발생시 오류 메시지 출력 const imgErrorHandler = () => { setHasImgError(true); setIsImgUrlConnectionError(true); @@ -80,7 +81,7 @@ export default function StylePreview({ onError={imgErrorHandler} />
-
+

{title || "타이틀을 입력해주세요"}

@@ -94,7 +95,7 @@ export default function StylePreview({ )} {selectedStyle === "카드" && ( -
+
-
- - 뒤로가기 - - -
-

링크 블록

-
- question -
- • 기본 정보와 공개 여부 값은 필수입니다.
• 예약 공개와 - 스티커는 프로 기능입니다. -
-
-
-
- - -
- - ); + return ; } diff --git a/src/app/admin/block/video/page.tsx b/src/app/admin/block/video/page.tsx index bd41e509..4885aedc 100644 --- a/src/app/admin/block/video/page.tsx +++ b/src/app/admin/block/video/page.tsx @@ -1,51 +1,57 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { FormEvent, useEffect, useRef, useState } from "react"; import Layout from "@app/admin/block/components/layout"; import TextInputBox from "@app/admin/block/components/text-input-box"; import AddButton from "@app/admin/block/components/buttons/add-button"; import ButtonBox from "@app/admin/block/components/buttons/button-box"; +import { useRouter } from "next/navigation"; +import { getSequence } from "../../../../lib/get-sequence"; +import FormInput from "@app/admin/block/components/form-input"; +import { checkUrl } from "../../../../lib/check-url"; const Page = () => { + const object = useRef(null); const [videoUrl, setVideoUrl] = useState(""); - const [iframeUrl, setIframeUrl] = useState(""); - - // const params = { - // type: 2, - // url: videoUrl, - // // sequence: number - // }; - const params = { - name: "test2000", - userId: "test2000", - password: "1234", - email: "test2000@google.com", - }; + const router = useRouter(); useEffect(() => { - signUp().then(); - }, []); + console.log(object.current?.validationMessage, "video"); + }, [videoUrl]); + + const addVideoBlock = async () => { + const token = sessionStorage.getItem("token"); + if (!token) { + router.push("/login"); + return; + } - const signUp = async () => { + const nowSequence = await getSequence(token); + console.log(nowSequence); + const params = { + type: 2, + url: videoUrl, + sequence: nowSequence + 1, + }; + console.log(nowSequence); try { const response = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/api/user/add`, + `${process.env.NEXT_PUBLIC_API_URL}/api/link/add`, { method: "POST", headers: { "Content-Type": "application/json", + Authorization: `Bearer ${token}`, }, body: JSON.stringify(params), }, ); if (response.ok) { - console.log(response); - alert("회원가입 완료"); + alert("비디오 블록 추가 완료"); + router.push("/admin"); } else { const { status } = response; - if (status === 400) { - alert("존재하는 아이디입니다."); - } + console.log(status); if (status === 500) { alert("서버 에러"); } @@ -63,32 +69,39 @@ const Page = () => { else return null; } - const handleAddButtonClick = () => { - const videoId = extractVideoID(videoUrl); - setIframeUrl( - videoId ? `https://www.youtube.com/embed/${videoId}` : videoUrl, - ); + const handleAddButtonClick = (e: FormEvent) => { + e.preventDefault(); + console.log("hi"); + addVideoBlock().then(); + }; + const setText = (text: string) => { + if (!checkUrl(text) && text !== "") { + alert("이미지 URL을 확인해주세요."); + return; + } + const videoId = extractVideoID(text); + setVideoUrl(videoId ? `https://www.youtube.com/embed/${videoId}` : text); }; return ( - - + setText(e.currentTarget.value)} + required /> - {iframeUrl && ( - -
hi
-
- )} +
+ {videoUrl && ( + +
동영상 주소를 확인해주세요
+
+ )} +
- +
); diff --git a/src/app/admin/components/calendar-block.tsx b/src/app/admin/components/calendar-block.tsx index ce0bf22f..2555386c 100644 --- a/src/app/admin/components/calendar-block.tsx +++ b/src/app/admin/components/calendar-block.tsx @@ -1,3 +1,18 @@ export default function CalendarBlock() { - return <>; + return ( + <> +
+
+
open
+
soon
+
closed
+
+
+
0개
+
1개
+
0개
+
+
+ + ); } diff --git a/src/app/admin/components/divide-block.tsx b/src/app/admin/components/divide-block.tsx index 4d39927d..76758d74 100644 --- a/src/app/admin/components/divide-block.tsx +++ b/src/app/admin/components/divide-block.tsx @@ -1,3 +1,13 @@ -export default function DivideBlock() { +interface DivideBlockProps { + type: number; + sequence: number; + style: number | null; +} + +export default function DivideBlock({ + type, + sequence, + style, +}: DivideBlockProps) { return <>; } diff --git a/src/app/admin/components/event-block.tsx b/src/app/admin/components/event-block.tsx index 53c1de3e..44634fec 100644 --- a/src/app/admin/components/event-block.tsx +++ b/src/app/admin/components/event-block.tsx @@ -1,3 +1,19 @@ export default function EventBlock() { - return <>; + return ( + <> +
+
+
이벤트 명
+ +
일정
+
+
+
10월 이벤트
+
+ 24.10.04 10:00 ~ 24.10.11 18:00 +
+
+
+ + ); } diff --git a/src/app/admin/components/link-block-sub/type-two.tsx b/src/app/admin/components/link-block-sub/type-two.tsx index a9b073dc..ef11a6a1 100644 --- a/src/app/admin/components/link-block-sub/type-two.tsx +++ b/src/app/admin/components/link-block-sub/type-two.tsx @@ -29,4 +29,4 @@ export default function TypeTwo({ url, style, imgUrl, title }: LinkBlockProps) { ); } -//섬네일 +//Thumbnail diff --git a/src/app/admin/components/video-block.tsx b/src/app/admin/components/video-block.tsx index 00dfffbf..efa1c6ff 100644 --- a/src/app/admin/components/video-block.tsx +++ b/src/app/admin/components/video-block.tsx @@ -1,7 +1,12 @@ +import Image from "next/image"; + export default function VideoBlock() { return ( <> -
d
+
+
+

+
); } diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index a5791e62..c13947aa 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; //components -import Navigation from "@app/(intro)/components/navigation"; +import Navigation from "@app/intro/components/navigation"; //styles diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index d93ef217..43227317 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,12 +1,15 @@ "use client"; -import BasicBlock from "@app/(intro)/components/basicblock"; +import BasicBlock from "@app/intro/components/basicblock"; import Image from "next/image"; import { useState, useEffect, useRef } from "react"; import Link from "next/link"; import { ClientRoute } from "@config/route"; -import EmptyBlock from "@app/(intro)/components/UI/empty-block"; +import EmptyBlock from "@app/intro/components/UI/empty-block"; import VideoBlock from "./components/video-block"; +import AddButton from "@app/admin/block/components/buttons/add-button"; +import ButtonBox from "@app/admin/block/components/buttons/button-box"; +import { useRouter } from "next/navigation"; interface Block { id: number; @@ -67,6 +70,7 @@ export default function Admin() { const dragItem = useRef(null); const dragOverItem = useRef(null); + const router = useRouter(); async function getBlocks() { const token = sessionStorage.getItem("token"); @@ -92,41 +96,88 @@ export default function Admin() { } } - const dragStart = (e: React.DragEvent, position: number) => { - dragItem.current = position; - console.log((e.target as HTMLDivElement).innerHTML); + const dragStart = (position: number) => { + dragItem.current = position; // position -> index (드래그 선택 아이템의 인덱스) }; - const dragEnter = (e: React.DragEvent, position: number) => { - dragOverItem.current = position; - console.log((e.target as HTMLDivElement).innerHTML); + const dragEnter = (position: number) => { + dragOverItem.current = position; // position -> index (드래그 오버 아이템의 인덱스) }; - const drop = (e: React.DragEvent) => { + const drop = () => { const copyListItems = [...blocks]; - const dragItemContent = copyListItems[dragItem.current as number]; - copyListItems.splice(dragItem.current as number, 1); - copyListItems.splice(dragOverItem.current as number, 0, dragItemContent); + const dragItemContent = copyListItems[dragItem.current as number]; // 리스트에서 드래그 선택 아이템 + copyListItems.splice(dragItem.current as number, 1); // 리스트에서 드래그 선택 아이템 삭제하여 리스트에서 제거 + copyListItems.splice(dragOverItem.current as number, 0, dragItemContent); // 리스트에서 드래그 오버 아이템의 위치에 드래그 선택 아이템 추가 + const newSequenceItems = copyListItems.map((item, index) => { + return { ...item, sequence: index }; + }); // 시퀀스 변경 + console.log(newSequenceItems); dragItem.current = null; dragOverItem.current = null; - setBlocks(copyListItems); + setBlocks(newSequenceItems); + }; + + const updateBlockOrder = async () => { + const token = sessionStorage.getItem("token"); + console.log(token); + if (!token) { + router.push("/login"); + return; + } + const block01sequence = blocks[0].sequence; + const block02sequence = blocks[1].sequence; + // const params = { + // order: [ + // { ...blocks[0], sequence: block02sequence }, + // { ...blocks[1], sequence: block01sequence }, + // ], + // }; + const params = { + order: blocks, + }; + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/link/update/order`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(params), + }, + ); + if (response.ok) { + alert("블록 순서 변경 완료"); + router.push("/admin"); + } else { + const { status } = response; + console.log(status); + if (status === 500) { + alert("서버 에러"); + } + } + } catch (error) { + alert("연결 실패"); + } }; return (
-
-
- profile - - momomoc - -
+
+ profile + + momomoc +

@@ -134,14 +185,22 @@ export default function Admin() {

방문자

-

전체 {showTotal}

-

오늘 {showToday}

-

실시간 {showRealTime}

+

+ 전체 {showTotal} +

+

+ 오늘 {showToday} +

+

+ 실시간 {showRealTime} +

소식받기

-

전체

+

+ 전체 0 +


@@ -189,6 +248,16 @@ export default function Admin() { /> )) )} +
+
+ +
+ +
); } diff --git a/src/app/(intro)/components/UI/empty-block.tsx b/src/app/intro/components/UI/empty-block.tsx similarity index 100% rename from src/app/(intro)/components/UI/empty-block.tsx rename to src/app/intro/components/UI/empty-block.tsx diff --git a/src/app/(intro)/components/UI/toggle-button.tsx b/src/app/intro/components/UI/toggle-button.tsx similarity index 100% rename from src/app/(intro)/components/UI/toggle-button.tsx rename to src/app/intro/components/UI/toggle-button.tsx diff --git a/src/app/(intro)/components/basicblock.tsx b/src/app/intro/components/basicblock.tsx similarity index 95% rename from src/app/(intro)/components/basicblock.tsx rename to src/app/intro/components/basicblock.tsx index 4eb066b5..80aeaefc 100644 --- a/src/app/(intro)/components/basicblock.tsx +++ b/src/app/intro/components/basicblock.tsx @@ -28,9 +28,9 @@ interface Block { dateCreate: string; dateUpdate: string | null; index: number; - dragStart: (e: React.DragEvent, position: number) => void; - dragEnter: (e: React.DragEvent, position: number) => void; - drop: (e: React.DragEvent) => void; + dragStart: (position: number) => void; + dragEnter: (position: number) => void; + drop: () => void; } export default function BasicBlock({ id, @@ -102,7 +102,7 @@ export default function BasicBlock({ function renderComponent(type: number) { switch (type) { case 1: - return ; + return ; case 2: return ; case 3: @@ -157,8 +157,8 @@ export default function BasicBlock({
dragStart(e, index)} - onDragEnter={(e) => dragEnter(e, index)} + onDragStart={() => dragStart(index)} + onDragEnter={() => dragEnter(index)} onDragEnd={drop} onDragOver={(e) => e.preventDefault()} > diff --git a/src/app/(intro)/components/error-boundary.tsx b/src/app/intro/components/error-boundary.tsx similarity index 100% rename from src/app/(intro)/components/error-boundary.tsx rename to src/app/intro/components/error-boundary.tsx diff --git a/src/app/(intro)/components/navigation.tsx b/src/app/intro/components/navigation.tsx similarity index 100% rename from src/app/(intro)/components/navigation.tsx rename to src/app/intro/components/navigation.tsx diff --git a/src/app/intro/page.tsx b/src/app/intro/page.tsx new file mode 100644 index 00000000..fddc7758 --- /dev/null +++ b/src/app/intro/page.tsx @@ -0,0 +1,30 @@ +import Link from "next/link"; +import { ClientRoute } from "@config/route"; + +export default function Intro() { + return ( +
+ {/* 스크린 리더 전용 텍스트*/} +

+ Link At Once! 소셜 프로필 링크 관리 서비스 in my link 입니다! +

+

시작하기 버튼을 눌러 로그인을 진행해 주세요.

+
+
+

+ link at once! +

+

+ Lorem ipsum dolor sit amet consectetur. +

+
+ + START! + +
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 19814b3e..17dc7516 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -18,7 +18,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + {children} ); diff --git a/src/app/link/page.tsx b/src/app/link/page.tsx deleted file mode 100644 index ab822bb6..00000000 --- a/src/app/link/page.tsx +++ /dev/null @@ -1,101 +0,0 @@ -export default function LinkPage() { - return ( - <> -
-
-

블록 링크

-
- - {/* 스타일 */} -
-
-
-
-
- link-icon -
-
-

타이틀을 입력해주세요

-
-
-
-
- -
-

- 스타일 * -

- {/* item * 4 */} -
-
- {/* style type img */} -
-

썸네일

-
-
-
- -
- - {/* Info */} -
-
-
- - -
- -
- - -
- - - -
-

이미지를 직접 끌어오거나

-

파일을 선택하여 업로드해주세요

-
-
-
- -
- - -
-
-
- - ); -} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 817f3eee..372ea7f9 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,10 +1,13 @@ "use client"; import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { ClientRoute } from "@config/route"; export default function Login() { const [userId, setUserId] = useState(""); const [password, setPassword] = useState(""); + const router = useRouter(); async function handleLogin(e: React.FormEvent) { e.preventDefault(); @@ -25,7 +28,11 @@ export default function Login() { const infor = await response.json(); if (response.ok) { alert("성공"); + // 세션 스토리지에 토큰 저장 sessionStorage.setItem("token", infor.data.token); + // 쿠키에 토큰 저장 + document.cookie = `token=${infor.data.token}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`; + router.push(ClientRoute.MAIN as string); } else { alert("실패"); } diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 00000000..023f57af --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { ClientRoute } from "@config/route"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +export default function Intro() { + const router = useRouter(); + + // 로그아웃 함수: 세션 스토리지와 쿠키에서 토큰 제거 후 리다이렉트 + async function handleLogout() { + try { + // 인증 관련 데이터 제거 + sessionStorage.removeItem("token"); + document.cookie = + "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + + await router.push(ClientRoute.MAIN as string); + router.refresh(); + } catch (error) { + console.error("로그아웃 중 오류 발생:", error); + } + } + + return ( + <> +

Home

+

in my link

+ + Admin + + + + + ); +} diff --git a/src/config/route.tsx b/src/config/route.tsx index 548783d3..57fb472e 100644 --- a/src/config/route.tsx +++ b/src/config/route.tsx @@ -1,13 +1,14 @@ import { ClientRouteType } from "@config/types"; export const ClientRoute: ClientRouteType = { - MAIN: "/", - LOGIN: "/login", - JOIN: "/join", - ADMIN: "/admin", - MY: "/admin/my", + MAIN: "/", // 메인 페이지 + INTRO: "/intro", // 랜딩 페이지 + LOGIN: "/login", // 로그인 페이지 + JOIN: "/join", // 회원가입 페이지 + ADMIN: "/admin", // 관리자 페이지 + MY: "/admin/my", // 개인 페이지 PROFILE: { - DETAIL: "/admin/profile/detail", - EDIT: "/admin/profile/edit", + DETAIL: "/admin/profile/detail", // 프로필 상세 페이지 + EDIT: "/admin/profile/edit", // 프로필 수정 페이지 }, }; diff --git a/src/lib/check-url.ts b/src/lib/check-url.ts new file mode 100644 index 00000000..710c7dd6 --- /dev/null +++ b/src/lib/check-url.ts @@ -0,0 +1,4 @@ +export const checkUrl = (strUrl: string) => { + const exp = /^http[s]?\:\/\//i; + return exp.test(strUrl); +}; diff --git a/src/lib/post-block.ts b/src/lib/post-block.ts new file mode 100644 index 00000000..048ae71c --- /dev/null +++ b/src/lib/post-block.ts @@ -0,0 +1,40 @@ +import { getSequence } from "./get-sequence"; +import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"; + +export const postBlock = async ( + path: string, + params: { [index: string]: string | number }, + router?: AppRouterInstance, +) => { + const token = sessionStorage.getItem("token"); + if (!token) { + if (router) router.push("/login"); + return; + } + params["sequence"] = (await getSequence(token)) + 1; + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${path}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(params), + }); + const responseJson = await response.json(); + if (response.ok) { + console.log("성공"); + return responseJson; + } else { + const { status } = response; + const { message } = responseJson; + if (status === 500) { + alert("서버 에러"); + } + console.log(`Error: ${status}, Message: ${message || "Unknown error"}`); + } + } catch (error) { + throw new Error(error instanceof Error ? error.message : "알 수 없는 에러"); + } +}; diff --git a/src/middleware.ts b/src/middleware.ts index b10d853d..798000a8 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -2,20 +2,37 @@ import { NextRequest, NextResponse } from "next/server"; export function middleware(request: NextRequest) { - // const token = request.cookies.get("token"); // 쿠키에서 인증 토큰을 가져옴 + const token = request.cookies.get("token"); - // if ( - // !token && - // request.nextUrl.pathname !== "/login" && - // request.nextUrl.pathname !== "/landing" - // ) { - // return NextResponse.redirect(new URL("/landing", request.url)); - // } + if ( + !token && + request.nextUrl.pathname !== "/login" && + request.nextUrl.pathname !== "/intro" + ) { + return NextResponse.redirect(new URL("/intro", request.url)); + } - return NextResponse.next(); // 로그인된 사용자에 대한 요청은 통과 + // 이미 로그인된 사용자가 로그인 또는 회원가입 페이지에 접근하면 메인 페이지로 리다이렉트 + if ( + token && + (request.nextUrl.pathname === "/login" || + request.nextUrl.pathname === "/join" || + request.nextUrl.pathname === "/intro") + ) { + return NextResponse.redirect(new URL("/", request.url)); + } + + return NextResponse.next(); } // 인증이 필요한 페이지 설정 export const config = { - matcher: ["/main", "/profile/:path*", "/admin"], // 인증이 필요한 경로 지정 + matcher: [ + "/", + "/profile/:path*", + "/admin/:path*", + "/login", + "/join", + "/intro", + ], // 인증이 필요한 경로 지정 }; diff --git a/src/styles/global.css b/src/styles/global.css index fc8a6334..60a92a06 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -32,9 +32,12 @@ body { color: var(--foreground); background: var(--background); font-family: Arial, Helvetica, sans-serif; - + -ms-overflow-style: none; @apply tracking-tight; } +::-webkit-scrollbar { + display: none; +} input { @apply h-12 w-full rounded-lg border-1 border-gray-200;