From fa94ab874728d39d25cf94dff7576d73cd59442d Mon Sep 17 00:00:00 2001 From: karnelll <165611407+karnelll@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:06:52 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Refactor:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fe/package-lock.json | 19 +++ fe/package.json | 1 + fe/src/app/api/auth/callback/naver/route.ts | 111 ----------------- fe/src/app/api/event/route.ts | 1 - fe/src/app/api/nomembers/pings/route.ts | 3 +- fe/src/app/api/nonmembers/pings/route.ts | 5 - fe/src/app/components/common/Button.tsx | 3 +- .../components/PincheckInput.tsx | 10 +- .../load-mappin-edit/components/ExitModal.tsx | 1 - .../[nonMemberId]/load-mappin-edit/page.tsx | 12 +- .../[nonMemberId]/stores/useUserDataStore.ts | 6 - .../[id]/components/BottomDrawer.tsx | 40 +++--- .../[id]/components/EventMapExitModal.tsx | 2 +- .../[id]/components/MapComponent.tsx | 6 +- .../[id]/components/RecommendActive.tsx | 3 - fe/src/app/event-maps/[id]/hooks/useDrawer.ts | 12 +- .../forms/links/\bcomponents/BottomSheet.tsx" | 9 +- .../forms/links/\bcomponents/CheckBox.tsx" | 4 +- .../[id]/load-mappin/forms/links/page.tsx | 26 ++-- .../forms/name-pin/components/NameField.tsx | 20 ++- .../forms/name-pin/components/PinField.tsx | 3 +- .../[id]/load-mappin/forms/name-pin/page.tsx | 9 +- .../[id]/load-mappin/forms/tooltip/page.tsx | 8 -- .../[id]/login/login/LoginModal.tsx | 91 ++++++++++++++ .../event-maps/[id]/login/login/LoginPin.tsx | 114 ++++++++++++++++++ fe/src/app/event-maps/[id]/login/page.tsx | 0 fe/src/app/event-maps/[id]/page.tsx | 9 +- .../event-maps/[id]/stores/useUpdateTime.ts | 4 +- .../components/EventNameInput.tsx | 18 +-- .../components/LocationInput.tsx | 2 - .../eventcreate-page/location-search/page.tsx | 12 +- fe/src/app/page.tsx | 6 - "fe/src/styles/\bglobals.css" | 7 +- fe/src/types/types.ts | 3 +- 34 files changed, 311 insertions(+), 269 deletions(-) delete mode 100644 fe/src/app/api/auth/callback/naver/route.ts create mode 100644 fe/src/app/event-maps/[id]/login/login/LoginModal.tsx create mode 100644 fe/src/app/event-maps/[id]/login/login/LoginPin.tsx create mode 100644 fe/src/app/event-maps/[id]/login/page.tsx diff --git a/fe/package-lock.json b/fe/package-lock.json index f2770ed..38ec23d 100644 --- a/fe/package-lock.json +++ b/fe/package-lock.json @@ -22,6 +22,7 @@ "react": "^18", "react-dom": "^18", "react-swipeable": "^7.0.2", + "swiper": "^11.1.15", "tailwind-scrollbar-hide": "^1.1.7", "uuid": "^10.0.0", "winston": "^3.14.2", @@ -6638,6 +6639,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swiper": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.1.15.tgz", + "integrity": "sha512-IzWeU34WwC7gbhjKsjkImTuCRf+lRbO6cnxMGs88iVNKDwV+xQpBCJxZ4bNH6gSrIbbyVJ1kuGzo3JTtz//CBw==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/synckit": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", diff --git a/fe/package.json b/fe/package.json index 9d15806..a204393 100644 --- a/fe/package.json +++ b/fe/package.json @@ -25,6 +25,7 @@ "react": "^18", "react-dom": "^18", "react-swipeable": "^7.0.2", + "swiper": "^11.1.15", "tailwind-scrollbar-hide": "^1.1.7", "uuid": "^10.0.0", "winston": "^3.14.2", diff --git a/fe/src/app/api/auth/callback/naver/route.ts b/fe/src/app/api/auth/callback/naver/route.ts deleted file mode 100644 index 7e30fd0..0000000 --- a/fe/src/app/api/auth/callback/naver/route.ts +++ /dev/null @@ -1,111 +0,0 @@ -// 백엔드와 연동 전 테스트 코드 -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(req: NextRequest) { - const { searchParams } = new URL(req.url); - const code = searchParams.get("code"); - const state = searchParams.get("state"); - const clientId = process.env.NEXT_PUBLIC_NAVER_CLIENT_ID; - const clientSecret = process.env.NEXT_PUBLIC_NAVER_CLIENT_SECRET; - const redirectUri = process.env.NEXT_PUBLIC_REDIRECT_URI; - - if (!code || !clientId || !clientSecret || !redirectUri) { - return NextResponse.json( - { error: "Missing required parameters" }, - { status: 400 } - ); - } - - // 네이버에 액세스 토큰 요청 - const tokenResponse = await fetch( - `https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=${clientId}&client_secret=${clientSecret}&code=${code}&state=${state}`, - { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - } - ); - - const tokenData = await tokenResponse.json(); - - if (tokenData.error) { - return NextResponse.json( - { error: tokenData.error_description }, - { status: 400 } - ); - } - - const accessToken = tokenData.access_token; - - // 액세스 토큰을 콘솔에 출력해 테스트 - console.log("Access Token:", accessToken); // eslint-disable-line no-console - - // 프론트엔드에서 액세스 토큰을 확인할 수 있도록 응답 - return NextResponse.json({ accessToken }); -} - -// 백엔드와 연동 후 사용 예정 코드 -/* -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(req: NextRequest) { - const { searchParams } = new URL(req.url); - const code = searchParams.get("code"); - const state = searchParams.get("state"); - const clientId = process.env.NEXT_PUBLIC_NAVER_CLIENT_ID; - const clientSecret = process.env.NEXT_PUBLIC_NAVER_CLIENT_SECRET; - const redirectUri = process.env.NEXT_PUBLIC_REDIRECT_URI; - - console.log("Code:", code); - console.log("State:", state); - console.log("Client ID:", clientId); - console.log("Client Secret:", clientSecret); - console.log("Redirect URI:", redirectUri); - - if (!code || !clientId || !clientSecret || !redirectUri) { - return NextResponse.json( - { error: "Missing required parameters" }, - { status: 400 } - ); - } - - try { - const tokenResponse = await fetch( - `https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=${clientId}&client_secret=${clientSecret}&code=${code}&state=${state}`, - { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - } - ); - - const tokenData = await tokenResponse.json(); - console.log("Token Data:", tokenData); - - if (tokenData.error) { - return NextResponse.json( - { error: tokenData.error_description }, - { status: 400 } - ); - } - - const accessToken = tokenData.access_token; - - const backendResponse = await fetch("http://localhost:8080/api/auth/naver", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ token: accessToken }), - }); - - if (backendResponse.ok) { - return NextResponse.redirect("/dashboard"); - } else { - return NextResponse.json( - { error: "Failed to authenticate with the backend" }, - { status: 500 } - ); - } - } catch (error) { - console.error("Error during OAuth process:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} -*/ diff --git a/fe/src/app/api/event/route.ts b/fe/src/app/api/event/route.ts index 9b6dd4a..05546f5 100644 --- a/fe/src/app/api/event/route.ts +++ b/fe/src/app/api/event/route.ts @@ -1,4 +1,3 @@ -// pages/api/event.ts import { NextResponse } from "next/server"; const BASE_API_URL = process.env.NEXT_PUBLIC_API_BASE_URL; diff --git a/fe/src/app/api/nomembers/pings/route.ts b/fe/src/app/api/nomembers/pings/route.ts index 88aaf8e..b8a02bb 100644 --- a/fe/src/app/api/nomembers/pings/route.ts +++ b/fe/src/app/api/nomembers/pings/route.ts @@ -1,7 +1,6 @@ -// pages/api/nonmembers/pings.ts import { NextRequest, NextResponse } from "next/server"; -const BASE_API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"; +const BASE_API_URL = process.env.NEXT_PUBLIC_API_URL export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); diff --git a/fe/src/app/api/nonmembers/pings/route.ts b/fe/src/app/api/nonmembers/pings/route.ts index 4eddf3f..831b0c5 100644 --- a/fe/src/app/api/nonmembers/pings/route.ts +++ b/fe/src/app/api/nonmembers/pings/route.ts @@ -13,7 +13,6 @@ export async function GET(request: NextRequest) { } try { - // 외부 API 요청 보내기 const externalResponse = await fetch(`/api/nonmembers/pings?uuid=${uuid}`, { method: "GET", headers: { @@ -21,7 +20,6 @@ export async function GET(request: NextRequest) { }, }); - // 외부 API 요청 실패 처리 if (!externalResponse.ok) { return NextResponse.json( { error: "Failed to fetch data from external API" }, @@ -29,13 +27,10 @@ export async function GET(request: NextRequest) { ); } - // JSON 데이터로 변환 const data = await externalResponse.json(); - // 외부 API에서 받은 데이터를 클라이언트로 반환 return NextResponse.json(data, { status: 200 }); } catch (error) { - // 오류 처리 console.error("Error fetching data:", error); return NextResponse.json( { error: "Internal Server Error" }, diff --git a/fe/src/app/components/common/Button.tsx b/fe/src/app/components/common/Button.tsx index 670c29d..bc5d367 100644 --- a/fe/src/app/components/common/Button.tsx +++ b/fe/src/app/components/common/Button.tsx @@ -10,7 +10,6 @@ function Button({ className = "", disabled = false, }: ButtonProps) { - // 기본 버튼 스타일 설정 const buttonStyle = disabled ? "bg-gray-200 text-gray-500 cursor-not-allowed" : "bg-[#1D1D1D] text-white"; @@ -18,7 +17,7 @@ function Button({ return (
- {/* Description */}

북마크 모음을 붙여넣으면 여러 곳을
한 번에 공유 가능!

- {/* Note */}
* 맘에 쏙 든 가게 하나만 공유도 가능해요
- {/* Inner Rectangle with GIF */}
- {/* 16px Spacing at the Bottom */}
diff --git "a/fe/src/app/event-maps/[id]/load-mappin/forms/links/\bcomponents/CheckBox.tsx" "b/fe/src/app/event-maps/[id]/load-mappin/forms/links/\bcomponents/CheckBox.tsx" index 719abcb..0137077 100644 --- "a/fe/src/app/event-maps/[id]/load-mappin/forms/links/\bcomponents/CheckBox.tsx" +++ "b/fe/src/app/event-maps/[id]/load-mappin/forms/links/\bcomponents/CheckBox.tsx" @@ -16,8 +16,8 @@ function CheckBox({ isChecked, onChange }: CheckBoxProps) { isChecked ? "border-[#B8B8B8]" : "border-[#e4e4e4]" } flex items-center justify-center`} onClick={onChange} - role="checkbox" // 명시적으로 checkbox 역할 부여 - aria-checked={isChecked} // aria-checked 속성 유지 + role="checkbox" + aria-checked={isChecked} aria-label="체크박스" > {isChecked && ( diff --git a/fe/src/app/event-maps/[id]/load-mappin/forms/links/page.tsx b/fe/src/app/event-maps/[id]/load-mappin/forms/links/page.tsx index c4459b1..1edd679 100644 --- a/fe/src/app/event-maps/[id]/load-mappin/forms/links/page.tsx +++ b/fe/src/app/event-maps/[id]/load-mappin/forms/links/page.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useEffect } from "react"; -import { useRouter, useParams } from "next/navigation"; // Correct order +import { useRouter, useParams } from "next/navigation"; import Navigation from "@/app/components/common/Navigation"; import Button from "@/app/components/common/Button"; import LinkField from "./components/LinkField"; @@ -82,10 +82,15 @@ export default function LinksPage() { return (
- - {/* Scrollable Content */} -
-
+ {/* 내비게이션 바 */} +
+ +
+ + {/* 메인 컨텐츠 */} +
+ {/* 타이틀 영역 */} +
마음에 쏙 든 공간을 불러와요
@@ -93,14 +98,12 @@ export default function LinksPage() {
- {/* 북마크 공유 링크 */} - {/* 가게 링크 */} - {/* CheckBox Section */} -
+ {/* 체크박스 */} +
내 공간 정보를 모핑에서 활용하는 것에 동의합니다 @@ -118,9 +121,9 @@ export default function LinksPage() {
- {/* Fixed Save Button */} + {/* 하단 버튼 */}
+ {/* 바텀 시트 */}
); diff --git a/fe/src/app/event-maps/[id]/load-mappin/forms/name-pin/components/NameField.tsx b/fe/src/app/event-maps/[id]/load-mappin/forms/name-pin/components/NameField.tsx index 75e9e79..079fe73 100644 --- a/fe/src/app/event-maps/[id]/load-mappin/forms/name-pin/components/NameField.tsx +++ b/fe/src/app/event-maps/[id]/load-mappin/forms/name-pin/components/NameField.tsx @@ -7,8 +7,8 @@ interface NameFieldProps { value: string; onChange: (value: string) => void; inputRef: React.RefObject; - onFocus?: () => void; // onFocus 속성 추가 - onBlur?: () => void; // onBlur 속성 추가 + onFocus?: () => void; + onBlur?: () => void; } export default function NameField({ @@ -20,8 +20,7 @@ export default function NameField({ }: NameFieldProps) { const [localErrorType, setLocalErrorType] = useState<"invalid" | null>(null); const [isFocused, setIsFocused] = useState(false); - const charLimit = 6; // Character limit - + const charLimit = 6; useEffect(() => { const storedName = localStorage.getItem("userName"); if (storedName) { @@ -32,12 +31,10 @@ export default function NameField({ const handleChange = (e: React.ChangeEvent) => { const inputValue = e.target.value; - // 글자 수 제한 조건 if (inputValue.length > charLimit) { - return; // 입력 중단 + return; } - // 유효성 검사 if (/^[ㄱ-ㅎ가-힣a-zA-Z]*$/.test(inputValue)) { setLocalErrorType(null); onChange(inputValue); @@ -55,16 +52,15 @@ export default function NameField({ const getInputBorderClass = () => { if (localErrorType) { - return "border-2 border-[#f73a2c] focus:ring-0"; // 에러 테두리 색상 + return "border-2 border-[#f73a2c] focus:ring-0"; } if (isFocused) { - return "border-2 border-[#555555] focus:ring-0"; // 활성화 테두리 색상 + return "border-2 border-[#555555] focus:ring-0"; } - return "border-transparent"; // 기본 테두리 + return "border-transparent"; }; useEffect(() => { - // Save to localStorage when the name is valid or changed if (localErrorType === null) { localStorage.setItem("userName", value); } @@ -110,7 +106,7 @@ export default function NameField({ )}
- {(isFocused || value) && ( // 입력 활성화 시만 메시지 표시 + {(isFocused || value) && (
([]); const [inputKeys, setInputKeys] = useState([]); - // 초기화 시 고유한 key 생성 useEffect(() => { setInputKeys((prevKeys) => value.length === prevKeys.length ? prevKeys : value.map(() => nanoid()) @@ -87,7 +86,7 @@ export default function PinField({ value, onChange }: PinFieldProps) {
{value.map((digit, index) => ( (null); - // 로컬 스토리지를 초기화하고 이름 입력 필드에 포커스 설정 useEffect(() => { localStorage.removeItem("userName"); localStorage.removeItem("userPin"); setName(""); setPin(["", "", "", ""]); - // 이름 입력 필드에 포커스 설정 nameInputRef.current?.focus(); }, []); - // PIN 입력 완료 여부 확인 const isPinComplete = pin.every((digit) => digit !== ""); - // 이름 입력 필드의 포커스가 해제될 때 입력 완료 여부 설정 const handleNameBlur = () => { if (name.trim() !== "") { setIsNameInputComplete(true); } }; - // 이름과 PIN이 모두 완성되었는지 확인하고 페이지 이동 useEffect(() => { if (isNameInputComplete && isPinComplete) { localStorage.setItem("userName", name); @@ -65,7 +60,7 @@ export default function NamePinPage() { value={name} onChange={setName} inputRef={nameInputRef} - onBlur={handleNameBlur} // 포커스 해제 시 호출 + onBlur={handleNameBlur} />
diff --git a/fe/src/app/event-maps/[id]/load-mappin/forms/tooltip/page.tsx b/fe/src/app/event-maps/[id]/load-mappin/forms/tooltip/page.tsx index cb9804d..6b48e40 100644 --- a/fe/src/app/event-maps/[id]/load-mappin/forms/tooltip/page.tsx +++ b/fe/src/app/event-maps/[id]/load-mappin/forms/tooltip/page.tsx @@ -67,8 +67,6 @@ export default function ToolTipPage() { {...handlers} > - - {/* Title Section */}

이렇게 공유 버튼이 안 보이나요? @@ -76,8 +74,6 @@ export default function ToolTipPage() { 이렇게 하면 해결 완

- - {/* Slide Content */}
{slides[currentSlide].step && ( @@ -107,8 +103,6 @@ export default function ToolTipPage() { />
- - {/* Slide Indicators */}
{slides.map((slide) => (
))}
- - {/* Button Section */}
+
+ ); +} diff --git a/fe/src/app/event-maps/[id]/login/page.tsx b/fe/src/app/event-maps/[id]/login/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/fe/src/app/event-maps/[id]/page.tsx b/fe/src/app/event-maps/[id]/page.tsx index 61c0459..eadd54b 100644 --- a/fe/src/app/event-maps/[id]/page.tsx +++ b/fe/src/app/event-maps/[id]/page.tsx @@ -103,7 +103,6 @@ export default function Page() { }; useDidMountEffect(() => { - // 메시지를 표시하고, 일정 시간 후에 숨깁니다. setIsVisible(true); const fadeOutTimer = setTimeout(() => { @@ -112,14 +111,14 @@ export default function Page() { const hideTimer = setTimeout(() => { setIsVisible(false); - setIsFadingOut(false); // 상태 초기화 + setIsFadingOut(false); }, 4500); return () => { clearTimeout(fadeOutTimer); clearTimeout(hideTimer); - }; // 타이머 정리 - }, [updateTime, trigger]); // updateTime이 변경될 때만 실행 + }; + }, [updateTime, trigger]); const bind = useDrag( ({ last, movement: [, my], memo = y.get() }) => { @@ -174,7 +173,7 @@ export default function Page() { `translateY(${val}px)`), // Use translateY from y value + transform: y.to((val) => `translateY(${val}px)`), touchAction: "none", }} className="w-full h-[218px] fixed bottom-0 z-10 bg-white shadow-lg rounded-t-lg" diff --git a/fe/src/app/event-maps/[id]/stores/useUpdateTime.ts b/fe/src/app/event-maps/[id]/stores/useUpdateTime.ts index 2b41b17..9f38ade 100644 --- a/fe/src/app/event-maps/[id]/stores/useUpdateTime.ts +++ b/fe/src/app/event-maps/[id]/stores/useUpdateTime.ts @@ -8,11 +8,11 @@ interface UpdateTimeState { const useUpdateTimeStore = create((set) => ({ updateTime: "방금", - trigger: false, // 트리거 상태 추가 + trigger: false, setUpdateTime: (time) => set((state) => ({ updateTime: time, - trigger: !state.trigger, // 트리거 상태를 토글 + trigger: !state.trigger, })), })); diff --git a/fe/src/app/eventcreate-page/components/EventNameInput.tsx b/fe/src/app/eventcreate-page/components/EventNameInput.tsx index 29df073..aec3654 100644 --- a/fe/src/app/eventcreate-page/components/EventNameInput.tsx +++ b/fe/src/app/eventcreate-page/components/EventNameInput.tsx @@ -25,14 +25,14 @@ function EventNameInput({ const handleInputChange = useCallback( (e: React.ChangeEvent) => { setHasUserEdited(true); - setIsTyping(true); // 타이핑 상태 활성화 + setIsTyping(true); onChange(e.target.value); }, [onChange] ); const handleBlur = useCallback(() => { - setIsTyping(false); // 타이핑 상태 비활성화 + setIsTyping(false); }, []); const handleClear = useCallback(() => { @@ -50,9 +50,9 @@ function EventNameInput({ className={classNames( "relative w-[328px] h-14 p-4 bg-[#f7f7f7] rounded-lg flex justify-between items-center border-2", { - "border-danger-base": showWarning, // 경고 상태 - "border-[#555555]": isTyping, // 타이핑 중 - "border-transparent": !isTyping && !showWarning, // 기본 상태 + "border-danger-base": showWarning, + "border-[#555555]": isTyping, + "border-transparent": !isTyping && !showWarning, } )} > @@ -60,14 +60,14 @@ function EventNameInput({ type="text" value={value} onChange={handleInputChange} - onBlur={handleBlur} // 포커스 해제 시 상태 변경 - onFocus={() => setIsTyping(true)} // 포커스 시 타이핑 활성화 + onBlur={handleBlur} + onFocus={() => setIsTyping(true)} placeholder="모임" className={classNames( "bg-transparent text-base font-medium font-['Pretendard'] leading-normal outline-none flex-grow", { - "text-[#2c2c2c]": isTyping || value.trim().length > 0, // 타이핑 중 또는 텍스트 입력 완료 - "text-[#8e8e8e]": !isTyping && value.trim().length === 0, // 기본 텍스트 색상 + "text-[#2c2c2c]": isTyping || value.trim().length > 0, + "text-[#8e8e8e]": !isTyping && value.trim().length === 0, } )} aria-label="이벤트 이름 입력" diff --git a/fe/src/app/eventcreate-page/components/LocationInput.tsx b/fe/src/app/eventcreate-page/components/LocationInput.tsx index 9aa9e1a..7f78c16 100644 --- a/fe/src/app/eventcreate-page/components/LocationInput.tsx +++ b/fe/src/app/eventcreate-page/components/LocationInput.tsx @@ -24,7 +24,6 @@ function LocationInput({ return (
- {/* Updated Label Styling */} @@ -40,7 +39,6 @@ function LocationInput({ height={24} className="mr-3" /> - {/* Updated Input Placeholder Styling */} (null); // 인풋 참조 + const inputRef = useRef(null); - // 장소 검색 함수 const fetchPlacesBySearch = useCallback( async (query: string) => { if (isFetching) return; @@ -48,10 +47,10 @@ function LocationSearch() { setIsFetching(false); } }, - [isFetching] // 의존성 배열에 isFetching 추가 + [isFetching] ); - const debouncedFetch = useRef(debounce(fetchPlacesBySearch, 300)).current; // debouncedFetch를 ref로 설정 + const debouncedFetch = useRef(debounce(fetchPlacesBySearch, 300)).current; useEffect(() => { if (location.trim()) { @@ -65,11 +64,10 @@ function LocationSearch() { }, [location, debouncedFetch]); useEffect(() => { - // 컴포넌트가 마운트될 때 인풋에 포커스를 주고 키보드를 열리게 합니다. if (inputRef.current) { setTimeout(() => { inputRef.current?.focus(); - }, 100); // 100ms 지연 후 포커스 + }, 100); } }, []); @@ -106,7 +104,7 @@ function LocationSearch() {
- {/* 상단 여백 */}
- {/* 위 선 */}
- {/* 로고 */}
- {/* 아래 선 */}
- {/* 아이콘들 */}
- {/* 모핑 시작하기 버튼 */}
)}
- {(isFocused || value) && ( + {(isFocused || value) && (
{value.map((digit, index) => ( (null); @@ -60,7 +60,7 @@ export default function NamePinPage() { value={name} onChange={setName} inputRef={nameInputRef} - onBlur={handleNameBlur} + onBlur={handleNameBlur} />
diff --git a/fe/src/app/event-maps/[id]/login/login/LoginModal.tsx b/fe/src/app/event-maps/[id]/login/login/LoginModal.tsx deleted file mode 100644 index 797fd89..0000000 --- a/fe/src/app/event-maps/[id]/login/login/LoginModal.tsx +++ /dev/null @@ -1,91 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; -import { Swiper, SwiperSlide } from "swiper/react"; -import "swiper/css"; - -type Profile = { - id: number; - name: string; - imageUrl: string; -}; - -export default function LoginModal({ - onProfileSelect, -}: { - onProfileSelect: (profile: Profile) => void; -}) { - const [profiles, setProfiles] = useState([]); - const [loading, setLoading] = useState(true); - const [isOpen, setIsOpen] = useState(false); - - useEffect(() => { - const fetchProfiles = async () => { - try { - const mockProfiles: Profile[] = [ - { id: 1, name: "John Doe", imageUrl: "/images/john.jpg" }, - { id: 2, name: "Jane Smith", imageUrl: "/images/jane.jpg" }, - { id: 3, name: "Alice Johnson", imageUrl: "/images/alice.jpg" }, - ]; - - setProfiles(mockProfiles); - } catch (error) { - console.error("Error fetching profiles:", error); - } finally { - setLoading(false); - } - }; - - const checkToken = async () => { - const isTokenValid = false; - - if (!isTokenValid) { - setIsOpen(true); - fetchProfiles(); - } else { - setIsOpen(false); - } - }; - - checkToken(); - }, []); - - if (!isOpen) return null; - - return ( -
-
-

로그인할 프로필을 선택하세요.

- {loading ? ( -

로딩 중...

- ) : ( - - {profiles.map((profile) => ( - -
{ - setIsOpen(false); - onProfileSelect(profile); - }} - > -
- {profile.name} -
-

{profile.name}

-
-
- ))} -
- )} -

- 아직 프로필 생성을 안했다면? -

-
-
- ); -} diff --git a/fe/src/app/event-maps/[id]/login/login/LoginPin.tsx b/fe/src/app/event-maps/[id]/login/login/LoginPin.tsx deleted file mode 100644 index 6f2b899..0000000 --- a/fe/src/app/event-maps/[id]/login/login/LoginPin.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import { useRouter, useParams, useSearchParams } from "next/navigation"; - -type Profile = { - id: number; - name: string; - imageUrl: string; -}; - -export default function LoginPin() { - const router = useRouter(); - const searchParams = useSearchParams(); - const { id } = useParams(); - const profileId = searchParams.get("profileId"); - - const [profile, setProfile] = useState(null); - const [password, setPassword] = useState(["", "", "", ""]); - const [currentIndex, setCurrentIndex] = useState(0); - const [hasError, setHasError] = useState(false); - - useEffect(() => { - const fetchProfile = async () => { - try { - const mockProfile: Profile = { - id: Number(profileId), - name: "John Doe", - imageUrl: "/images/john.jpg", - }; - - setProfile(mockProfile); - } catch (error) { - console.error("Error fetching profile:", error); - } - }; - - if (profileId) { - fetchProfile(); - } - }, [profileId]); - - const handlePasswordSubmit = async () => { - const fullPassword = password.join(""); - - if (fullPassword.length !== 4) { - setHasError(true); - return; - } - - try { - const isPasswordCorrect = fullPassword === "1234"; - - if (isPasswordCorrect) { - router.push(`/event-maps/${id}`); - } else { - setHasError(true); - setPassword(["", "", "", ""]); - setCurrentIndex(0); - } - } catch (error) { - setHasError(true); - } - }; - - if (!profile) return

Loading...

; - - return ( -
-

{profile.name}님의 비밀번호를 입력하세요

-
- {password.map((_, i) => ( - { - const value = e.target.value; - if (/^\d$/.test(value)) { - const newPassword = [...password]; - newPassword[i] = value; - setPassword(newPassword); - - if (i < 3) setCurrentIndex(i + 1); - } - }} - onKeyDown={(e) => { - if (e.key === "Backspace" && i > 0) { - const newPassword = [...password]; - newPassword[i] = ""; - setPassword(newPassword); - setCurrentIndex(i - 1); - } - }} - className={`w-10 h-10 text-center ${ - hasError ? "border-red-500" : "border-gray-300" - }`} - /> - ))} -
- - {hasError &&

비밀번호가 잘못되었습니다.

} - - -
- ); -} diff --git a/fe/src/app/event-maps/[id]/login/page.tsx b/fe/src/app/event-maps/[id]/login/page.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/fe/src/app/event-maps/[id]/page.tsx b/fe/src/app/event-maps/[id]/page.tsx index eadd54b..5bac949 100644 --- a/fe/src/app/event-maps/[id]/page.tsx +++ b/fe/src/app/event-maps/[id]/page.tsx @@ -111,14 +111,14 @@ export default function Page() { const hideTimer = setTimeout(() => { setIsVisible(false); - setIsFadingOut(false); + setIsFadingOut(false); }, 4500); return () => { clearTimeout(fadeOutTimer); clearTimeout(hideTimer); - }; - }, [updateTime, trigger]); + }; + }, [updateTime, trigger]); const bind = useDrag( ({ last, movement: [, my], memo = y.get() }) => { @@ -173,7 +173,7 @@ export default function Page() { `translateY(${val}px)`), + transform: y.to((val) => `translateY(${val}px)`), touchAction: "none", }} className="w-full h-[218px] fixed bottom-0 z-10 bg-white shadow-lg rounded-t-lg" diff --git a/fe/src/app/event-maps/[id]/stores/useUpdateTime.ts b/fe/src/app/event-maps/[id]/stores/useUpdateTime.ts index 9f38ade..0ec01cc 100644 --- a/fe/src/app/event-maps/[id]/stores/useUpdateTime.ts +++ b/fe/src/app/event-maps/[id]/stores/useUpdateTime.ts @@ -8,11 +8,11 @@ interface UpdateTimeState { const useUpdateTimeStore = create((set) => ({ updateTime: "방금", - trigger: false, + trigger: false, setUpdateTime: (time) => set((state) => ({ updateTime: time, - trigger: !state.trigger, + trigger: !state.trigger, })), })); diff --git a/fe/src/app/eventcreate-page/components/EventNameInput.tsx b/fe/src/app/eventcreate-page/components/EventNameInput.tsx index aec3654..5925b3b 100644 --- a/fe/src/app/eventcreate-page/components/EventNameInput.tsx +++ b/fe/src/app/eventcreate-page/components/EventNameInput.tsx @@ -25,14 +25,14 @@ function EventNameInput({ const handleInputChange = useCallback( (e: React.ChangeEvent) => { setHasUserEdited(true); - setIsTyping(true); + setIsTyping(true); onChange(e.target.value); }, [onChange] ); const handleBlur = useCallback(() => { - setIsTyping(false); + setIsTyping(false); }, []); const handleClear = useCallback(() => { @@ -50,9 +50,9 @@ function EventNameInput({ className={classNames( "relative w-[328px] h-14 p-4 bg-[#f7f7f7] rounded-lg flex justify-between items-center border-2", { - "border-danger-base": showWarning, - "border-[#555555]": isTyping, - "border-transparent": !isTyping && !showWarning, + "border-danger-base": showWarning, + "border-[#555555]": isTyping, + "border-transparent": !isTyping && !showWarning, } )} > @@ -60,13 +60,13 @@ function EventNameInput({ type="text" value={value} onChange={handleInputChange} - onBlur={handleBlur} - onFocus={() => setIsTyping(true)} + onBlur={handleBlur} + onFocus={() => setIsTyping(true)} placeholder="모임" className={classNames( "bg-transparent text-base font-medium font-['Pretendard'] leading-normal outline-none flex-grow", { - "text-[#2c2c2c]": isTyping || value.trim().length > 0, + "text-[#2c2c2c]": isTyping || value.trim().length > 0, "text-[#8e8e8e]": !isTyping && value.trim().length === 0, } )} diff --git a/fe/src/app/eventcreate-page/location-search/page.tsx b/fe/src/app/eventcreate-page/location-search/page.tsx index f537ae2..be73ff5 100644 --- a/fe/src/app/eventcreate-page/location-search/page.tsx +++ b/fe/src/app/eventcreate-page/location-search/page.tsx @@ -14,7 +14,7 @@ function LocationSearch() { const [isFetching, setIsFetching] = useState(false); const router = useRouter(); const { setLocation: setStoreLocation } = useLocationStore(); - const inputRef = useRef(null); + const inputRef = useRef(null); const fetchPlacesBySearch = useCallback( async (query: string) => { @@ -47,10 +47,10 @@ function LocationSearch() { setIsFetching(false); } }, - [isFetching] + [isFetching] ); - const debouncedFetch = useRef(debounce(fetchPlacesBySearch, 300)).current; + const debouncedFetch = useRef(debounce(fetchPlacesBySearch, 300)).current; useEffect(() => { if (location.trim()) { @@ -67,7 +67,7 @@ function LocationSearch() { if (inputRef.current) { setTimeout(() => { inputRef.current?.focus(); - }, 100); + }, 100); } }, []); @@ -104,7 +104,7 @@ function LocationSearch() {
void; - type?: "start" | "next" | "submit"; + type?: "start" | "next" | "submit"; className?: string; disabled?: boolean; } From 79656fd88a4c2068c1cdf5220990ed85298ffd2a Mon Sep 17 00:00:00 2001 From: karnelll <165611407+karnelll@users.noreply.github.com> Date: Wed, 27 Nov 2024 03:03:01 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[feat]:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fe/public/svg/ModalDelete.svg | 7 + .../load-mappin-edit/components/EditLink.tsx | 102 +++------ .../[nonMemberId]/load-mappin-edit/page.tsx | 4 +- .../[id]/components/BottomDrawer.tsx | 13 +- .../forms/links/\bcomponents/LinkField.tsx" | 6 +- .../[id]/load-mappin/forms/links/page.tsx | 15 +- .../[id]/login/components/LoginModal.tsx | 203 ++++++++++++++++++ fe/src/app/event-maps/[id]/login/page.tsx | 14 ++ .../pin-check/components/LoginPinCheck.tsx | 165 ++++++++++++++ .../event-maps/[id]/login/pin-check/page.tsx | 52 +++++ fe/src/app/event-maps/[id]/page.tsx | 2 +- 11 files changed, 489 insertions(+), 94 deletions(-) create mode 100644 fe/public/svg/ModalDelete.svg create mode 100644 fe/src/app/event-maps/[id]/login/components/LoginModal.tsx create mode 100644 fe/src/app/event-maps/[id]/login/page.tsx create mode 100644 fe/src/app/event-maps/[id]/login/pin-check/components/LoginPinCheck.tsx create mode 100644 fe/src/app/event-maps/[id]/login/pin-check/page.tsx diff --git a/fe/public/svg/ModalDelete.svg b/fe/public/svg/ModalDelete.svg new file mode 100644 index 0000000..d5a5ceb --- /dev/null +++ b/fe/public/svg/ModalDelete.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx index e875d0b..a6b9a2e 100644 --- a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx +++ b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx @@ -3,15 +3,12 @@ import React, { useState, useEffect, useRef } from "react"; import { nanoid } from "nanoid"; import Image from "next/image"; -import { useRouter, useParams } from "next/navigation"; interface LinkFieldEditProps { label: string; placeholder: string; value: string[]; onChange: (value: string[]) => void; - showTooltip?: boolean; - onInfoClick?: () => void; } interface InputField { @@ -20,9 +17,9 @@ interface InputField { error: string; isValid: boolean; isTyping: boolean; - canEdit: boolean; } +// 전체 코드에 에러가 발생할 가능성이 있는 부분 수정 export default function LinkFieldEdit({ label, placeholder, @@ -37,7 +34,6 @@ export default function LinkFieldEdit({ error: "", isValid: true, isTyping: false, - canEdit: true, })) : [ { @@ -46,15 +42,13 @@ export default function LinkFieldEdit({ error: "", isValid: false, isTyping: false, - canEdit: true, }, ] ); const inputRefs = useRef<(HTMLInputElement | null)[]>([]); - const router = useRouter(); - const { id } = useParams(); + // 업데이트된 유효 링크 관리 useEffect(() => { const validLinks = inputFields .filter((field) => field.isValid) @@ -69,7 +63,7 @@ export default function LinkFieldEdit({ const response = await fetch( `${process.env.NEXT_PUBLIC_API_BASE_URL}${endpoint}`, { - method: "POST", + method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }), } @@ -119,19 +113,29 @@ export default function LinkFieldEdit({ : fieldItem ) ); - - validateLink(fieldId, inputValue, label); }; - const handleFocus = (fieldId: string) => { - setInputFields((prevFields) => - prevFields.map((fieldItem) => - fieldItem.id === fieldId ? { ...fieldItem, isTyping: true } : fieldItem - ) - ); + const handlePaste = (fieldId: string, event: React.ClipboardEvent) => { + const pastedText = event.clipboardData.getData("Text").trim(); + + if (pastedText) { + setInputFields((prevFields) => + prevFields.map((fieldItem) => + fieldItem.id === fieldId + ? { ...fieldItem, text: pastedText, isValid: false, isTyping: true } + : fieldItem + ) + ); + validateLink(fieldId, pastedText, label); + } }; const handleBlur = (fieldId: string) => { + const field = inputFields.find((fieldItem) => fieldItem.id === fieldId); + if (field && field.text) { + validateLink(fieldId, field.text, label); + } + setInputFields((prevFields) => prevFields.map((fieldItem) => fieldItem.id === fieldId ? { ...fieldItem, isTyping: false } : fieldItem @@ -148,7 +152,6 @@ export default function LinkFieldEdit({ error: "", isValid: false, isTyping: false, - canEdit: true, }, ]); }; @@ -163,18 +166,6 @@ export default function LinkFieldEdit({ ); }; - const navigateToTooltipPage = () => { - if (id) { - router.push(`/event-maps/${id}/load-mappin/forms/tooltip`); - } else { - console.error("ID not found for navigation"); - } - }; - - const handleNaverMove = () => { - window.location.href = "https://m.map.naver.com/"; - }; - const getClassNames = (item: InputField): string => { if (item.error && !item.isTyping) { return "border-2 border-[#f73a2c] bg-[#F8F8F8]"; @@ -192,51 +183,14 @@ export default function LinkFieldEdit({
{inputFields.map((item, index) => (
handleFocus(item.id)} onChange={(e) => handleInputChange(item.id, e.target.value)} + onPaste={(e) => handlePaste(item.id, e)} onBlur={() => handleBlur(item.id)} placeholder={placeholder} - className={`flex-1 bg-transparent outline-none placeholder:text-[#8e8e8e] text-sm font-medium font-['Pretendard'] ${ - item.isValid ? "text-[#3A91EA]" : "" - }`} + className="flex-1 bg-transparent outline-none placeholder:text-[#8e8e8e] text-sm font-medium font-['Pretendard']" /> {item.text && (
- {item.error && ( + {!item.isTyping && item.error && (
{item.error}
diff --git a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/page.tsx b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/page.tsx index ca7940f..82b29ef 100644 --- a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/page.tsx +++ b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/page.tsx @@ -78,7 +78,7 @@ export default function LinkEditPage() { try { const response = await fetch( - `${process.env.NEXT_PUBLIC_API_BASE_URL}/nonmembers/pings`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/pings`, { method: "PUT", headers: { @@ -151,7 +151,7 @@ export default function LinkEditPage() { value={storeLinks} onChange={setStoreLinks} /> -
+
setIsChecked(!isChecked)} diff --git a/fe/src/app/event-maps/[id]/components/BottomDrawer.tsx b/fe/src/app/event-maps/[id]/components/BottomDrawer.tsx index 0be7fad..e37e5f6 100644 --- a/fe/src/app/event-maps/[id]/components/BottomDrawer.tsx +++ b/fe/src/app/event-maps/[id]/components/BottomDrawer.tsx @@ -79,7 +79,7 @@ export function BottomDrawer({ useEffect(() => { const fetchAllPings = async () => { try { - const response = await fetch(`${apiUrl}/nonmembers/pings?uuid=${id}`); + const response = await fetch(`${apiUrl}/pings?uuid=${id}`); if (response.ok) { const data = await response.json(); let recommendProfile = []; @@ -145,7 +145,7 @@ export function BottomDrawer({ }; try { - const response = await fetch(`${apiUrl}/nonmembers/pings/recommend`, { + const response = await fetch(`${apiUrl}/pings/recommend`, { method: "POST", headers: { "Content-Type": "application/json;charset=UTF-8", @@ -172,7 +172,7 @@ export function BottomDrawer({ try { const response = await fetch( - `${apiUrl}/nonmembers/pings/recommend?uuid=${id}&radiusInKm=${Km}`, + `${apiUrl}/pings/recommend?uuid=${id}&radiusInKm=${Km}`, { method: "GET" } ); if (response.ok) { @@ -220,10 +220,9 @@ export function BottomDrawer({ const handleRefresh = async () => { try { - const response = await fetch( - `${apiUrl}/nonmembers/pings/refresh-all?uuid=${id}`, - { method: "GET" } - ); + const response = await fetch(`${apiUrl}/pings/refresh-all?uuid=${id}`, { + method: "GET", + }); if (response.ok) { const data = await response.json(); let recommendProfile = []; diff --git "a/fe/src/app/event-maps/[id]/load-mappin/forms/links/\bcomponents/LinkField.tsx" "b/fe/src/app/event-maps/[id]/load-mappin/forms/links/\bcomponents/LinkField.tsx" index a45aea7..e244ed7 100644 --- "a/fe/src/app/event-maps/[id]/load-mappin/forms/links/\bcomponents/LinkField.tsx" +++ "b/fe/src/app/event-maps/[id]/load-mappin/forms/links/\bcomponents/LinkField.tsx" @@ -69,7 +69,7 @@ export default function LinkField({ const response = await fetch( `${process.env.NEXT_PUBLIC_API_BASE_URL}${endpoint}`, { - method: "POST", + method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }), } @@ -124,8 +124,8 @@ export default function LinkField({ validateLink(fieldId, clipboardText, label); } - } catch { - // 클립보드 읽기 실패 시 처리 + } catch (error) { + console.error("클립보드에서 텍스트를 읽는 데 실패했습니다:", error); } }; diff --git a/fe/src/app/event-maps/[id]/load-mappin/forms/links/page.tsx b/fe/src/app/event-maps/[id]/load-mappin/forms/links/page.tsx index 1edd679..2a310b6 100644 --- a/fe/src/app/event-maps/[id]/load-mappin/forms/links/page.tsx +++ b/fe/src/app/event-maps/[id]/load-mappin/forms/links/page.tsx @@ -51,7 +51,7 @@ export default function LinksPage() { try { setIsSubmitting(true); const response = await fetch( - `${process.env.NEXT_PUBLIC_API_BASE_URL}/nonmembers/pings`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/pings`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -62,11 +62,17 @@ export default function LinksPage() { if (response.ok) { localStorage.clear(); router.push(`/event-maps/${id}`); + } else if (response.status === 409) { + // 405 에러 처리 + alert("이미 존재하는 이름입니다. 다시 시도해주세요."); } else { setIsFormComplete(false); + alert("저장에 실패했습니다. 다시 시도해주세요."); } - } catch { + } catch (error) { + console.error("저장 중 에러 발생:", error); setIsFormComplete(false); + alert("네트워크 오류가 발생했습니다. 다시 시도해주세요."); } finally { setIsSubmitting(false); } @@ -82,15 +88,12 @@ export default function LinksPage() { return (
- {/* 내비게이션 바 */}
- {/* 메인 컨텐츠 */}
- {/* 타이틀 영역 */} -
+
마음에 쏙 든 공간을 불러와요
diff --git a/fe/src/app/event-maps/[id]/login/components/LoginModal.tsx b/fe/src/app/event-maps/[id]/login/components/LoginModal.tsx new file mode 100644 index 0000000..ea6124b --- /dev/null +++ b/fe/src/app/event-maps/[id]/login/components/LoginModal.tsx @@ -0,0 +1,203 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useSwipeable } from "react-swipeable"; +import { useRouter } from "next/navigation"; +import Image from "next/image"; + +interface NonMember { + nonMemberId: number; + name: string; + profileSvg: string; +} + +interface LoginModalProps { + eventId: string; +} + +export default function LoginModal({ eventId }: LoginModalProps) { + const [profiles, setProfiles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isOpen, setIsOpen] = useState(true); + const [currentPage, setCurrentPage] = useState(0); + const profilesPerPage = 4; + + const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL; + const router = useRouter(); + + const handleNextPage = () => { + if (currentPage < Math.ceil(profiles.length / profilesPerPage) - 1) { + setCurrentPage((prev) => prev + 1); + } + }; + + const handlePrevPage = () => { + if (currentPage > 0) { + setCurrentPage((prev) => prev - 1); + } + }; + + useEffect(() => { + const fetchProfiles = async () => { + setLoading(true); + setError(null); + + try { + const response = await fetch(`${apiUrl}/pings?uuid=${eventId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch profiles: ${response.status}`); + } + + const data = await response.json(); + setProfiles(data.nonMembers || []); + } catch (err: unknown) { + setError("프로필 데이터를 불러올 수 없습니다. 다시 시도해주세요."); + } finally { + setLoading(false); + } + }; + + fetchProfiles(); + }, [eventId, apiUrl]); + + const closeModal = () => { + setIsOpen(false); + }; + + const navigateToPasswordPage = (nonMemberId: number) => { + router.push( + `/event-maps/${eventId}/login/pin-check?nonMemberId=${nonMemberId}` + ); + }; + + const swipeHandlers = useSwipeable({ + onSwipedLeft: handleNextPage, + onSwipedRight: handlePrevPage, + preventScrollOnSwipe: true, + }); + + const currentProfiles = profiles.slice( + currentPage * profilesPerPage, + (currentPage + 1) * profilesPerPage + ); + + const navigateToProfileCreation = () => { + router.push(`/event-maps/${eventId}/load-mappin/forms/name-pin`); + }; + + return ( + <> + {isOpen && ( +
+
+
+ +
+ 로그인할 프로필을 +
+ 선택하세요. +
+ +
+ {loading && ( +

로딩 중...

+ )} + {!loading && error && ( +

{error}

+ )} + {!loading && !error && profiles.length === 0 && ( +

+ 저장된 프로필이 없습니다. +

+ )} + {!loading && !error && profiles.length > 0 && ( +
+ {currentProfiles.map((profile) => ( +
+ navigateToPasswordPage(profile.nonMemberId) + } + role="button" + tabIndex={0} + className="w-[68px] flex flex-col items-center cursor-pointer" + onKeyDown={(e) => + e.key === "Enter" && + navigateToPasswordPage(profile.nonMemberId) + } + > +
+ {profile.name} +
+

+ {profile.name} +

+
+ ))} +
+ )} +
+ +
+ {Array.from( + { length: Math.ceil(profiles.length / profilesPerPage) }, + (_, index) => ( +
+ +
+ e.key === "Enter" && navigateToProfileCreation() + } + className="left-[66px] top-[348px] absolute text-center text-[#8e8e8e] text-xs font-medium font-['Pretendard'] underline leading-none cursor-pointer" + > + 아직 프로필 생성을 안했다면? +
+ + +
+
+ )} + + ); +} diff --git a/fe/src/app/event-maps/[id]/login/page.tsx b/fe/src/app/event-maps/[id]/login/page.tsx new file mode 100644 index 0000000..41ef5f8 --- /dev/null +++ b/fe/src/app/event-maps/[id]/login/page.tsx @@ -0,0 +1,14 @@ +// 로그인 모달창을 구현하기 위한 임시 페이지입니다. +// 로그인 컴포넌트를 아래와 같은 식으로 사용하고 싶은 곳에 붙여 넣으면 됩니다. + +import LoginModal from "./components/LoginModal"; + +export default function Page({ params }: { params: { id: string } }) { + const { id } = params; + return ( +
+ {" "} + {/* LoginModal 컴포넌트에 id를 eventId로 전달 */} +
+ ); +} diff --git a/fe/src/app/event-maps/[id]/login/pin-check/components/LoginPinCheck.tsx b/fe/src/app/event-maps/[id]/login/pin-check/components/LoginPinCheck.tsx new file mode 100644 index 0000000..dca62c2 --- /dev/null +++ b/fe/src/app/event-maps/[id]/login/pin-check/components/LoginPinCheck.tsx @@ -0,0 +1,165 @@ +"use client"; + +import React, { useRef, useState, useEffect, useCallback } from "react"; +import Image from "next/image"; +import { useSearchParams } from "next/navigation"; +import { v4 as uuidv4 } from "uuid"; // UUID를 생성하기 위해 추가 + +export interface PasswordInputProps { + iconUrl: string | null; +} + +export default function PasswordInput({ iconUrl }: PasswordInputProps) { + const [password, setPassword] = useState(["", "", "", ""]); + const [currentIndex, setCurrentIndex] = useState(0); + const [hasError, setHasError] = useState(false); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + const searchParams = useSearchParams(); + + const nonMemberId = searchParams.get("nonMemberId"); + + const submitPassword = useCallback(async () => { + const fullPassword = password.join(""); + + if (fullPassword.length !== 4) { + setHasError(true); + return; + } + + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/nonmembers/login-token`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + nonMemberId: Number(nonMemberId), + password: fullPassword, + }), + } + ); + + if (response.ok) { + const { accessToken } = await response.json(); + + // 토큰을 로컬 스토리지에 저장 + localStorage.setItem("authToken", accessToken); + + // 일단 임시로 성공 메시지 넣어놨어 + alert("로그인이 완료되었습니다!"); + + // 페이지 이동(호야 이거 너가 설정해) + window.location.href = `/event-maps/${searchParams.get("eventId")}`; + } else { + setHasError(true); + setPassword(["", "", "", ""]); + setCurrentIndex(0); + } + } catch (error) { + setHasError(true); + alert("로그인 API 호출에 실패했습니다. 다시 시도해주세요."); + } + }, [nonMemberId, password, searchParams]); + + useEffect(() => { + if (password.every((digit) => digit !== "")) { + submitPassword(); + } + }, [password, submitPassword]); + + const handleInputChange = ( + e: React.ChangeEvent, + index: number + ) => { + const inputValue = e.target.value; + + if (/^\d$/.test(inputValue)) { + const newPass = [...password]; + newPass[index] = inputValue; + setPassword(newPass); + + if (index < password.length - 1) { + setCurrentIndex(index + 1); + } + } + }; + + const handleKeyDown = ( + e: React.KeyboardEvent, + index: number + ) => { + if (e.key === "Backspace") { + const newPass = [...password]; + newPass[index] = ""; + + if (index > 0) { + setCurrentIndex(index - 1); + } + + setPassword(newPass); + setHasError(false); + } + }; + + useEffect(() => { + inputRefs.current[currentIndex]?.focus(); + }, [currentIndex]); + + return ( +
+ {/* 잠금 아이콘 */} +
+ {iconUrl ? ( + Lock Icon + ) : ( +
+ )} +
+ +

+ 암호를 입력하세요 +

+
+ {password.map((_, i) => ( +
+ { + inputRefs.current[i] = el; + }} + type="text" + inputMode="numeric" + pattern="[0-9]*" + className="grow text-center w-full h-full bg-transparent outline-none text-2xl" + maxLength={1} + value={password[i]} + onChange={(e) => handleInputChange(e, i)} + onKeyDown={(e) => handleKeyDown(e, i)} + /> +
+ ))} +
+ + {hasError && ( +

+ 암호가 일치하지 않아요 +

+ )} +
+ ); +} diff --git a/fe/src/app/event-maps/[id]/login/pin-check/page.tsx b/fe/src/app/event-maps/[id]/login/pin-check/page.tsx new file mode 100644 index 0000000..507ee24 --- /dev/null +++ b/fe/src/app/event-maps/[id]/login/pin-check/page.tsx @@ -0,0 +1,52 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import NavBar from "@/app/components/common/Navigation"; +import PasswordInput from "./components/LoginPinCheck"; + +export default function PasswordPage() { + const searchParams = useSearchParams(); + const nonMemberId = searchParams.get("nonMemberId"); + const [profileData, setProfileData] = useState<{ + profileLockSvg: string; + } | null>(null); + + useEffect(() => { + const fetchProfileData = async () => { + if (!nonMemberId) { + alert("nonMemberId가 없습니다."); + return; + } + + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/nonmembers/${nonMemberId}` + ); + if (response.ok) { + const data = await response.json(); + setProfileData({ profileLockSvg: data.profileLockSvg }); + } else { + alert("프로필 데이터를 불러오지 못했습니다."); + } + } catch (error) { + alert(`API 호출 실패: ${error}`); + } + }; + + fetchProfileData(); + }, [nonMemberId]); + + return ( +
+ +
+ {profileData ? ( + + ) : ( +

로딩 중...

+ )} +
+
+ ); +} diff --git a/fe/src/app/event-maps/[id]/page.tsx b/fe/src/app/event-maps/[id]/page.tsx index 5bac949..5aa3bae 100644 --- a/fe/src/app/event-maps/[id]/page.tsx +++ b/fe/src/app/event-maps/[id]/page.tsx @@ -54,7 +54,7 @@ export default function Page() { const fetchData = async () => { try { const response = await fetch( - `${process.env.NEXT_PUBLIC_API_BASE_URL}/nonmembers/pings?uuid=${id}`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/pings?uuid=${id}`, { method: "GET", headers: { From f60b93d5b8d32f4e4a2567c8ca615fff61406dae Mon Sep 17 00:00:00 2001 From: karnelll <165611407+karnelll@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:50:59 +0900 Subject: [PATCH 4/5] [Refactor] Apply cleanURL function and input handling improvements --- .../load-mappin-edit/components/EditLink.tsx | 78 ++++++++++------- .../[nonMemberId]/load-mappin-edit/page.tsx | 27 +++--- .../forms/links/\bcomponents/LinkField.tsx" | 83 +++++-------------- 3 files changed, 79 insertions(+), 109 deletions(-) diff --git a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx index a6b9a2e..45f3d87 100644 --- a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx +++ b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx @@ -19,7 +19,6 @@ interface InputField { isTyping: boolean; } -// 전체 코드에 에러가 발생할 가능성이 있는 부분 수정 export default function LinkFieldEdit({ label, placeholder, @@ -48,7 +47,6 @@ export default function LinkFieldEdit({ const inputRefs = useRef<(HTMLInputElement | null)[]>([]); - // 업데이트된 유효 링크 관리 useEffect(() => { const validLinks = inputFields .filter((field) => field.isValid) @@ -56,6 +54,11 @@ export default function LinkFieldEdit({ onChange(validLinks); }, [inputFields, onChange]); + const cleanURL = (url: string): string => { + const match = url.match(/https?:\/\/[^\s]+/); + return match ? match[0].trim() : ""; + }; + const validateLink = async (fieldId: string, url: string, type: string) => { const endpoint = type === "북마크 공유 링크" ? "/pings/bookmark" : "/pings/store"; @@ -89,8 +92,7 @@ export default function LinkFieldEdit({ ) ); } - } catch (error) { - console.error("API 요청 실패:", error); + } catch { setInputFields((prevFields) => prevFields.map((fieldItem) => fieldItem.id === fieldId @@ -105,37 +107,52 @@ export default function LinkFieldEdit({ } }; + const handlePasteFromClipboard = async (fieldId: string) => { + try { + const clipboardText = await navigator.clipboard.readText(); + const cleanedValue = cleanURL(clipboardText); // URL 정리 + if (cleanedValue) { + setInputFields((prevFields) => + prevFields.map((fieldItem) => + fieldItem.id === fieldId + ? { ...fieldItem, text: cleanedValue, isValid: false } + : fieldItem + ) + ); + + validateLink(fieldId, cleanedValue, label); + } + } catch (error) { + console.error("클립보드에서 텍스트를 읽는 데 실패했습니다:", error); + } + }; + const handleInputChange = (fieldId: string, inputValue: string) => { + const cleanedValue = cleanURL(inputValue); // URL 정리 setInputFields((prevFields) => prevFields.map((fieldItem) => fieldItem.id === fieldId - ? { ...fieldItem, text: inputValue, isValid: false, isTyping: true } + ? { ...fieldItem, text: cleanedValue, isValid: false, isTyping: true } : fieldItem ) ); + + if (cleanedValue) { + validateLink(fieldId, cleanedValue, label); + } }; - const handlePaste = (fieldId: string, event: React.ClipboardEvent) => { - const pastedText = event.clipboardData.getData("Text").trim(); + const handleFocus = (fieldId: string) => { + setInputFields((prevFields) => + prevFields.map((fieldItem) => + fieldItem.id === fieldId ? { ...fieldItem, isTyping: true } : fieldItem + ) + ); - if (pastedText) { - setInputFields((prevFields) => - prevFields.map((fieldItem) => - fieldItem.id === fieldId - ? { ...fieldItem, text: pastedText, isValid: false, isTyping: true } - : fieldItem - ) - ); - validateLink(fieldId, pastedText, label); - } + handlePasteFromClipboard(fieldId); }; const handleBlur = (fieldId: string) => { - const field = inputFields.find((fieldItem) => fieldItem.id === fieldId); - if (field && field.text) { - validateLink(fieldId, field.text, label); - } - setInputFields((prevFields) => prevFields.map((fieldItem) => fieldItem.id === fieldId ? { ...fieldItem, isTyping: false } : fieldItem @@ -167,15 +184,10 @@ export default function LinkFieldEdit({ }; const getClassNames = (item: InputField): string => { - if (item.error && !item.isTyping) { + if (item.error && !item.isTyping) return "border-2 border-[#f73a2c] bg-[#F8F8F8]"; - } - if (item.isValid) { - return "bg-[#EBF4FD] text-[#3a91ea]"; - } - if (item.isTyping) { - return "border-2 border-[#555555] bg-[#F8F8F8]"; - } + if (item.isValid) return "bg-[#EBF4FD] text-[#3a91ea]"; + if (item.isTyping) return "border-2 border-[#555555] bg-[#F8F8F8]"; return "bg-[#F8F8F8]"; }; @@ -206,11 +218,13 @@ export default function LinkFieldEdit({ }} type="text" value={item.text} + onFocus={() => handleFocus(item.id)} onChange={(e) => handleInputChange(item.id, e.target.value)} - onPaste={(e) => handlePaste(item.id, e)} onBlur={() => handleBlur(item.id)} placeholder={placeholder} - className="flex-1 bg-transparent outline-none placeholder:text-[#8e8e8e] text-sm font-medium font-['Pretendard']" + className={`flex-1 bg-transparent outline-none placeholder:text-[#8e8e8e] text-sm font-medium font-['Pretendard'] ${ + item.isValid ? "text-[#3A91EA]" : "" + }`} /> {item.text && (