Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/app/admin/block/link/components/form-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
type FormInputProps = {
label: string;
id: string;
selectedStyle?: string;
} & React.ComponentPropsWithoutRef<"input">;

export default function FormInput({
label,
id,
selectedStyle,
...inputProps
}: FormInputProps) {
return (
<div>
<label className="title mb-[10px] block" htmlFor={id}>
{label} <span className="text-red-500">*</span>
{label}
{selectedStyle !== "심플" && <span className="text-red-500">*</span>}
</label>
<input id={id} {...inputProps} />
</div>
Expand Down
106 changes: 79 additions & 27 deletions src/app/admin/block/link/components/link-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"use client";

import { FormEvent, useEffect, useState } from "react";
import {
ChangeEvent,
FormEvent,
useCallback,
useEffect,
useState,
} from "react";
import StylePreview from "./style-preview";
import StyleType from "./style-type";
import FormInput from "./form-input";
Expand Down Expand Up @@ -43,7 +49,14 @@ export default function LinkForm() {
const [title, setTitle] = useState("");
const [linkUrl, setLinkUrl] = useState("");
const [linkImg, setLinkImg] = useState("");
const [isImageError, setIsImageError] = useState(false);
const [isLinkUrlError, setIsLinkUrlError] = useState(false);
const [isImgUrlError, setIsImgUrlError] = useState(false);
const [isImgUrlConnectionError, setIsImgUrlConnectionError] = useState(false);

const isValidUrl = useCallback(
(url: string) => /^https?:\/\/.+\..+/.test(url),
[],
);

async function postLink() {
const token = await getToken();
Expand Down Expand Up @@ -89,7 +102,7 @@ export default function LinkForm() {
}

useEffect(() => {
if (linkImg) setIsImageError(false);
if (linkImg) setIsImgUrlConnectionError(false);
}, [linkImg]);

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
Expand All @@ -101,13 +114,33 @@ export default function LinkForm() {
setLinkImg("");
};

const handleLinkUrlChange = (e: ChangeEvent<HTMLInputElement>) => {
const newUrl = e.target.value;
setLinkUrl(newUrl);
if (newUrl.trim() === "") {
setIsLinkUrlError(false);
} else {
setIsLinkUrlError(!isValidUrl(newUrl));
}
};
const handleImgUrlChange = (e: ChangeEvent<HTMLInputElement>) => {
const newUrl = e.target.value;
setLinkImg(newUrl);
if (newUrl.trim() === "") {
setIsImgUrlError(false);
} else {
setIsImgUrlError(!isValidUrl(newUrl));
}
};

return (
<>
<StylePreview
selectedStyle={selectedStyle}
title={title}
linkImg={linkImg}
setIsImageError={setIsImageError}
setIsImgUrlConnectionError={setIsImgUrlConnectionError}
isValidUrl={isValidUrl}
/>

<form onSubmit={handleSubmit} className="mt-8">
Expand All @@ -124,6 +157,8 @@ export default function LinkForm() {
imgIdx={idx}
selectedStyle={selectedStyle}
onSelect={setSelectedStyle}
setLinkImg={setLinkImg}
setIsImgUrlConnectionError={setIsImgUrlConnectionError}
/>
))}
</div>
Expand All @@ -132,38 +167,55 @@ export default function LinkForm() {
<div className="my-8 border-t-2 border-[#F6F6F6]"></div>

{/* Info */}
<section className="flex flex-col gap-8">
<FormInput
label="연결할 주소"
type="url"
id="linked-url"
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
placeholder="연결할 주소 url을 입력해주세요"
required
/>
<FormInput
label="타이틀"
type="text"
id="link-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="타이틀을 입력해주세요"
required
/>
<div className="h-[110px]">
<section className="flex flex-col gap-4">
<div className="min-h-[110px]">
<FormInput
label="연결할 주소"
type="url"
id="linked-url"
value={linkUrl}
onChange={handleLinkUrlChange}
placeholder="연결할 주소 url을 입력해주세요"
required
/>
{isLinkUrlError && (
<div className="mt-1 text-sm text-red-500">
올바른 URL 형식을 입력해주세요
</div>
)}
</div>
<div className="min-h-[110px]">
<FormInput
label="타이틀"
type="text"
id="link-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="타이틀을 입력해주세요"
required
/>
</div>
<div className="min-h-[110px]">
<FormInput
label="이미지"
type="url"
id="linked-img"
value={linkImg}
onChange={(e) => setLinkImg(e.target.value)}
onChange={handleImgUrlChange}
selectedStyle={selectedStyle}
placeholder="이미지 url을 입력해주세요"
disabled={selectedStyle === "심플"}
required={selectedStyle !== "심플"}
/>
{isImageError && (
<div className="mt-1 text-red-500">잘못된 이미지 경로입니다</div>
{isImgUrlError && (
<div className="mt-1 text-sm text-red-500">
올바른 URL 형식을 입력해주세요
</div>
)}
{isImgUrlConnectionError && (
<div className="mt-1 text-sm text-red-500">
잘못된 이미지 경로입니다
</div>
)}
</div>
</section>
Expand Down
18 changes: 9 additions & 9 deletions src/app/admin/block/link/components/style-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ export default function StylePreview({
selectedStyle,
title,
linkImg,
setIsImageError,
setIsImgUrlConnectionError,
isValidUrl,
}: {
selectedStyle: string;
title: string;
linkImg: string;
setIsImageError: Dispatch<SetStateAction<boolean>>;
setIsImgUrlConnectionError: Dispatch<SetStateAction<boolean>>;
isValidUrl: (url: string) => boolean;
}) {
const placeholderImage =
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
Expand All @@ -24,7 +26,7 @@ export default function StylePreview({
useEffect(() => {
if (selectedStyle === "배경" && linkImg && isValidUrl(linkImg)) {
setHasImgError(false);
setIsImageError(false);
setIsImgUrlConnectionError(false);

// 이미지 로드 비동기 처리
const img = new window.Image();
Expand All @@ -36,22 +38,20 @@ export default function StylePreview({

img.onerror = () => {
setHasImgError(true); // 이미지 로드 실패
setIsImageError(true);
setIsImgUrlConnectionError(true);
};
} else if (!isValidUrl(linkImg)) {
// URL이 유효하지 않으면 에러 상태 설정하지 않음
setHasImgError(false);
setIsImageError(false);
setIsImgUrlConnectionError(false);
}
}, [linkImg, selectedStyle, setIsImageError]);
}, [linkImg, selectedStyle, setIsImgUrlConnectionError, isValidUrl]);

const imgErrorHandler = () => {
setHasImgError(true);
setIsImageError(true);
setIsImgUrlConnectionError(true);
};

const isValidUrl = (url: string) => /^https?:\/\/.+\..+/.test(url);

const imgUrl =
!hasImgError && isValidUrl(linkImg) ? linkImg : placeholderImage;

Expand Down
15 changes: 14 additions & 1 deletion src/app/admin/block/link/components/style-type.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import Image from "next/image";
import { Dispatch, SetStateAction } from "react";
import { twMerge } from "tailwind-merge";

export default function StyleType({
name,
imgIdx,
selectedStyle,
onSelect,
setLinkImg,
setIsImgUrlConnectionError,
}: {
name: string;
imgIdx: number;
selectedStyle: string;
onSelect: (style: string) => void;
setLinkImg: Dispatch<SetStateAction<string>>;
setIsImgUrlConnectionError: Dispatch<SetStateAction<boolean>>;
}) {
const isSelected = selectedStyle === name;

function clickHandler() {
onSelect(name);
if (selectedStyle === "심플") {
setLinkImg("");
setIsImgUrlConnectionError(false);
}
}

return (
<label
htmlFor={name}
key={name}
className="flex w-[185px] cursor-pointer flex-col items-center"
onClick={() => onSelect(name)}
onClick={clickHandler}
>
<input type="radio" value={name} id={name} className="hidden" />
<div
Expand Down
3 changes: 2 additions & 1 deletion src/app/admin/block/link/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ export default function LinkPage() {
width={30}
height={30}
/>
<div className="absolute left-full top-1/2 w-max -translate-y-1/2 translate-x-2 transform rounded bg-[#343434] px-2 py-1 text-sm text-white opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<div className="absolute left-full top-1/2 w-max -translate-y-1/2 translate-x-2 transform rounded bg-[#343434] px-3 py-3 text-sm text-white opacity-0 transition-opacity duration-300 group-hover:opacity-100">
• 기본 정보와 공개 여부 값은 필수입니다. <br />• 예약 공개와
스티커는 프로 기능입니다.
<div className="absolute -left-[5px] top-1/2 h-0 w-0 -translate-y-1/2 border-y-8 border-r-8 border-y-transparent border-r-[#343434]"></div>
</div>
</div>
</div>
Expand Down