From 94f62db9db6c489dd3f989af48c6c7b1e88e6f28 Mon Sep 17 00:00:00 2001 From: surinkwon Date: Mon, 17 Jun 2024 12:10:40 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=99=B8=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../landing/link/LandingLinkModal.tsx | 72 ++++++++++++++++--- frontend/src/test/isValidURL.test.ts | 18 +++++ frontend/src/utils/isValidURL.ts | 16 +++++ 3 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 frontend/src/test/isValidURL.test.ts create mode 100644 frontend/src/utils/isValidURL.ts diff --git a/frontend/src/components/landing/link/LandingLinkModal.tsx b/frontend/src/components/landing/link/LandingLinkModal.tsx index fe8aebc..50b712a 100644 --- a/frontend/src/components/landing/link/LandingLinkModal.tsx +++ b/frontend/src/components/landing/link/LandingLinkModal.tsx @@ -1,34 +1,84 @@ -import { MouseEventHandler } from "react"; +import { ChangeEvent, MouseEventHandler, useState } from "react"; +import isValidURL from "../../../utils/isValidURL"; const LandingLinkModal = ({ close }: { close: () => void }) => { + const [linkData, setLinkData] = useState({ + url: "", + description: "", + }); + + const handleInputChange = ( + { target }: ChangeEvent, + field: "url" | "description" + ) => { + const { value } = target; + + setLinkData({ ...linkData, [field]: decodeURI(value) }); + }; + + const handleConfirmClick = () => { + const { url, description } = linkData; + const errorTextList = []; + + if (!isValidURL(url)) { + errorTextList.push("유효하지 않은 URL입니다."); + } + + if (url.trim().length > 255 || description.trim().length > 255) { + errorTextList.push("링크 주소와 이름은 255자 이하여야 합니다."); + } + + if (url.trim() === "" || description.trim() === "") { + errorTextList.push("링크 주소와 이름은 모두 입력되어야 합니다."); + } + + if (errorTextList.length) { + alert(errorTextList.join("\n")); + return; + } + }; + const handleCloseClick: MouseEventHandler< HTMLButtonElement | HTMLDivElement > = ({ target, currentTarget }: React.MouseEvent) => { - if (target !== currentTarget) return; + if (target !== currentTarget) { + return; + } close(); }; return (
-

| 링크 추가하기

-
+

| 링크 추가하기

+

링크 주소

- + handleInputChange(event, "url")} + />
-
+

링크 이름

- + handleInputChange(event, "description")} + />
-
- +
); }; diff --git a/frontend/src/components/landing/link/LandingLinkModal.tsx b/frontend/src/components/landing/link/LandingLinkModal.tsx index 0867341..ed7d50d 100644 --- a/frontend/src/components/landing/link/LandingLinkModal.tsx +++ b/frontend/src/components/landing/link/LandingLinkModal.tsx @@ -62,7 +62,7 @@ const LandingLinkModal = ({ return (
diff --git a/frontend/src/hooks/common/landing/useLandingLinkSocket.ts b/frontend/src/hooks/common/landing/useLandingLinkSocket.ts index 6ae368f..33165cf 100644 --- a/frontend/src/hooks/common/landing/useLandingLinkSocket.ts +++ b/frontend/src/hooks/common/landing/useLandingLinkSocket.ts @@ -8,10 +8,10 @@ import { } from "../../../types/common/landing"; const useLandingLinkSocket = (socket: Socket) => { - const [link, setLink] = useState([]); + const [linkList, setLinkList] = useState([]); const handleInitEvent = (content: LandingDTO) => { - const { link } = content as LandingDTO; - setLink(link); + const { linkList } = content as LandingDTO; + setLinkList(linkList); }; const handleLinkEvent = ( @@ -20,7 +20,10 @@ const useLandingLinkSocket = (socket: Socket) => { ) => { switch (action) { case LandingSocketLinkAction.CREATE: - setLink([...link, content]); + setLinkList([...linkList, content]); + break; + case LandingSocketLinkAction.DELETE: + setLinkList(linkList.filter(({ id }) => id !== content.id)); break; } }; @@ -43,6 +46,10 @@ const useLandingLinkSocket = (socket: Socket) => { socket.emit("link", { action: "create", content }); }; + const emitLinkDeleteEvent = (content: { id: number }) => { + socket.emit("link", { action: "delete", content }); + }; + useEffect(() => { socket.on("landing", handleOnLanding); @@ -51,7 +58,7 @@ const useLandingLinkSocket = (socket: Socket) => { }; }, []); - return { link, emitLinkCreateEvent }; + return { linkList, emitLinkCreateEvent, emitLinkDeleteEvent }; }; export default useLandingLinkSocket; diff --git a/frontend/src/types/DTO/landingDTO.ts b/frontend/src/types/DTO/landingDTO.ts index 4f6a6f5..4d152ae 100644 --- a/frontend/src/types/DTO/landingDTO.ts +++ b/frontend/src/types/DTO/landingDTO.ts @@ -46,6 +46,6 @@ export interface LandingDTO { myInfo: LandingMemberDTO; sprint: LandingSprintDTO | null; memoList: LandingMemoDTO[]; - link: LandingLinkDTO[]; + linkList: LandingLinkDTO[]; inviteLinkId: string; } From ff91db8863fcfe6ced621f92dd0ec6994ccd5c11 Mon Sep 17 00:00:00 2001 From: surinkwon Date: Tue, 18 Jun 2024 10:29:23 +0900 Subject: [PATCH 6/8] =?UTF-8?q?style:=20svg=EC=9D=98=20fill=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=84=A4=EC=A0=95=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/icons/trash-can.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/assets/icons/trash-can.svg b/frontend/src/assets/icons/trash-can.svg index 56e723a..ac53335 100644 --- a/frontend/src/assets/icons/trash-can.svg +++ b/frontend/src/assets/icons/trash-can.svg @@ -1,3 +1,3 @@ - + From 6a3dad7158af1309bc8a658cac785cf6b654600a Mon Sep 17 00:00:00 2001 From: surinkwon Date: Tue, 18 Jun 2024 11:43:58 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EC=99=B8=EB=B6=80=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 확인 버튼을 누르면 한 번에 유효하지 않은 이유를 alert하던 것을 사용자 편의성을 위해 입력할 때마다 피드백을 주는 방식으로 변경 --- .../landing/link/LandingLinkModal.tsx | 122 +++++++++++++----- 1 file changed, 90 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/landing/link/LandingLinkModal.tsx b/frontend/src/components/landing/link/LandingLinkModal.tsx index ed7d50d..1c3e113 100644 --- a/frontend/src/components/landing/link/LandingLinkModal.tsx +++ b/frontend/src/components/landing/link/LandingLinkModal.tsx @@ -7,6 +7,13 @@ interface LandingLinkModalProps { emitLinkCreateEvent: (content: { url: string; description: string }) => void; } +interface ValidDataState { + urlValid: boolean | null; + urlValidText: string; + descriptionValid: boolean | null; + descriptionValidText: string; +} + const LandingLinkModal = ({ close, emitLinkCreateEvent, @@ -15,35 +22,76 @@ const LandingLinkModal = ({ url: "", description: "", }); + const [validData, setValidData] = useState({ + urlValid: null, + urlValidText: "", + descriptionValid: null, + descriptionValidText: "", + }); - const handleInputChange = ( - { target }: ChangeEvent, - field: "url" | "description" - ) => { - const { value } = target; - - setLinkData({ ...linkData, [field]: decodeURI(value) }); - }; + const checkUrlValidation = (url: string): [boolean, string] => { + if (url.trim().length === 0) { + return [false, "링크 주소를 입력해주세요."]; + } - const handleConfirmClick = () => { - const { url, description } = linkData; - const errorTextList = []; - const urlWithScheme = addSchemeToURL(url); + if (decodeURI(addSchemeToURL(url)).length > 255) { + return [false, "링크 주소의 길이가 너무 깁니다."]; + } - if (!isValidURL(urlWithScheme)) { - errorTextList.push("유효하지 않은 URL입니다."); + if (!isValidURL(decodeURI(addSchemeToURL(url)))) { + return [false, "유효하지 않은 링크 주소입니다."]; } - if (urlWithScheme.trim().length > 255 || description.trim().length > 255) { - errorTextList.push("링크 주소와 이름은 255자 이하여야 합니다."); + return [true, ""]; + }; + + const checkDescriptionValidation = ( + description: string + ): [boolean, string] => { + if (description.length > 255) { + return [false, "링크 이름의 길이가 너무 깁니다."]; } - if (url.trim() === "" || description.trim() === "") { - errorTextList.push("링크 주소와 이름은 모두 입력되어야 합니다."); + if (description.trim().length === 0) { + return [false, "링크 이름을 입력해주세요."]; } - if (errorTextList.length) { - alert(errorTextList.join("\n")); + return [true, ""]; + }; + + const handleUrlChange = ({ target }: ChangeEvent) => { + const { value } = target; + const [urlValid, urlValidText] = checkUrlValidation(value); + + setLinkData({ ...linkData, url: decodeURI(value) }); + setValidData({ ...validData, urlValid, urlValidText }); + }; + + const handleDescriptionChange = ({ + target, + }: ChangeEvent) => { + const { value } = target; + const [descriptionValid, descriptionValidText] = + checkDescriptionValidation(value); + + setLinkData({ ...linkData, description: value }); + setValidData({ ...validData, descriptionValid, descriptionValidText }); + }; + + const handleConfirmClick = () => { + const { url, description } = linkData; + const urlWithScheme = decodeURI(addSchemeToURL(url)); + const [urlValid, urlValidText] = checkUrlValidation(urlWithScheme); + const [descriptionValid, descriptionValidText] = + checkDescriptionValidation(description); + + if (!urlValid || !descriptionValid) { + setValidData({ + urlValid, + urlValidText, + descriptionValid, + descriptionValidText, + }); return; } @@ -67,21 +115,31 @@ const LandingLinkModal = ({ >

| 링크 추가하기

-
+

링크 주소

- handleInputChange(event, "url")} - /> +
+ +

+ {validData.urlValidText} +

+
-
+

링크 이름

- handleInputChange(event, "description")} - /> +
+ +

+ {validData.descriptionValidText} +

+