Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fix] 이벤트 생성 QA 반영 #52

Merged
merged 1 commit into from
Nov 3, 2024
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
46 changes: 32 additions & 14 deletions fe/src/app/components/common/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,47 @@
import React from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { NavigationProps } from "@/types/types";

function Navigation({ showBackButton = true }: NavigationProps) {
// Directly define NavigationProps in this file
interface NavigationProps {
showBackButton?: boolean;
title?: string;
onBack?: () => void;
}

function Navigation({
showBackButton = true,
title = "",
onBack,
}: NavigationProps) {
const router = useRouter();

const handleBackClick = () => {
router.back();
if (onBack) {
onBack(); // Use the provided onBack function if available
} else {
router.back(); // Otherwise, use router.back()
}
};

return (
<header className="nav-bar sticky top-[36px] z-10">
<header className="fixed top-0 left-1/2 transform -translate-x-1/2 w-[360px] h-[56px] bg-white flex items-center justify-between px-4 z-10">
{showBackButton && (
<div className="absolute left-0">
<button type="button" onClick={handleBackClick} className="p-2">
<Image
src="/images/ArrowBack.svg"
alt="뒤로가기"
width={24}
height={24}
/>
</button>
</div>
<button type="button" onClick={handleBackClick} className="p-2">
<Image
src="/images/ArrowBack.svg"
alt="뒤로가기"
width={24}
height={24}
/>
</button>
)}
<h1
className="text-lg font-semibold text-[#2c2c2c] mx-auto"
style={{ width: "320px", textAlign: "center" }}
>
{title}
</h1>
</header>
);
}
Expand Down
63 changes: 19 additions & 44 deletions fe/src/app/eventcreate-page/components/EventNameInput.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// components/EventNameInput.tsx

"use client";

import React, { useEffect, useState } from "react";
import Image from "next/image";
import { EventNameInputProps } from "@/app/eventcreate-page/types/types";

// 현재 날짜를 "MM.DD" 형식으로 반환하는 함수
const getCurrentDate = () => {
const today = new Date();
const month = String(today.getMonth() + 1).padStart(2, "0");
Expand All @@ -18,19 +19,16 @@ function EventNameInput({
onChange,
value,
}: EventNameInputProps) {
const [isFocused, setIsFocused] = useState(false);
const [hasUserEdited, setHasUserEdited] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const currentDate = getCurrentDate();

useEffect(() => {
if (!hasUserEdited) {
const newEventName = selectedLocation
? `${currentDate} ${selectedLocation} 모임`
: `${currentDate} 모임`;
if (!hasUserEdited && selectedLocation) {
const newEventName = `${currentDate} ${selectedLocation} 모임`;
onChange(newEventName);
} else if (!selectedLocation && !hasUserEdited) {
onChange("");
}
setIsLoading(false);
}, [selectedLocation, currentDate, onChange, hasUserEdited]);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -44,42 +42,29 @@ function EventNameInput({
onChange("");
};

const borderClass = isFocused ? "border-[#2C2C2C]" : "border-transparent";

// 기본 값일 때는 #8e8e8e 색상으로, 그렇지 않을 경우 #2c2c2c 색상으로 설정
const textColorClass =
value === `${currentDate} 모임` ||
value === `${currentDate} ${selectedLocation} 모임`
? "text-[#8e8e8e]"
: "text-[#2c2c2c]";

const charCount = value.length;
const showWarning = charCount < 1 || charCount > 20;
const isDefaultValue =
value === `${currentDate} 모임` ||
value === `${currentDate} ${selectedLocation} 모임`;
const borderClass = "border-transparent";
const showWarning = hasUserEdited && value.trim().length < 1;

return (
<div className={`relative flex flex-col ${className}`}>
<div className="text-black text-xl font-semibold leading-loose mb-[12px]">
<div className={`relative flex flex-col ${className} mt-4`}>
<div className="text-[#2c2c2c] text-lg font-semibold font-['Pretendard'] leading-relaxed mb-2">
이벤트 이름
</div>

<div
className={`relative w-[328px] h-14 p-4 bg-background-light rounded-lg flex justify-between items-center border-2 ${
showWarning && !isLoading ? "border-danger-base" : borderClass
className={`relative w-[328px] h-14 p-4 bg-[#f7f7f7] rounded-lg flex justify-between items-center border-2 ${
showWarning ? "border-danger-base" : borderClass
}`}
>
<input
type="text"
value={value}
onChange={handleInputChange}
className={`bg-transparent border-none grow shrink basis-0 ${textColorClass} text-base font-medium font-['Pretendard'] leading-normal outline-none flex-1`}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder={`${currentDate} 모임`}
className="bg-transparent border-none grow shrink basis-0 text-[#2c2c2c] text-base font-medium font-['Pretendard'] leading-normal outline-none flex-1"
/>

{value && !isDefaultValue && (
{value && (
<div
role="button"
tabIndex={0}
Expand All @@ -99,20 +84,10 @@ function EventNameInput({
)}
</div>

{!isLoading && (
<>
{showWarning ? (
<div className="text-danger-base text-sm font-medium font-['Pretendard'] leading-tight mt-2">
글자 수는 1 - 20자 사이로 입력해주세요
</div>
) : (
!isDefaultValue && (
<div className="text-right text-mediumGray text-sm font-medium font-['Pretendard'] leading-tight mt-2">
{charCount}/20
</div>
)
)}
</>
{showWarning && (
<div className="text-danger-base text-sm font-medium font-['Pretendard'] leading-tight mt-2">
모임명은 1자 이상 작성 가능해요
</div>
)}
</div>
);
Expand Down
182 changes: 39 additions & 143 deletions fe/src/app/eventcreate-page/components/LocationInput.tsx
Original file line number Diff line number Diff line change
@@ -1,130 +1,49 @@
import React, { useState, useEffect, useCallback } from "react";
import debounce from "lodash.debounce";
import Image from "next/image";
import SearchResults from "./SearchResults";

interface Place {
name: string;
address: string;
px?: number;
py?: number;
}

interface LocationInputProps {
className?: string;
onSelect: (place: Place) => void;
}

function LocationInput({ className, onSelect }: LocationInputProps) {
const [location, setLocation] = useState<string>("");
const [results, setResults] = useState<Place[]>([]);
const [isFetching, setIsFetching] = useState(false);

// 검색 API 호출 함수, useCallback으로 감싸서 의존성 안정화
const fetchPlacesBySearch = useCallback(
async (query: string) => {
if (isFetching) return;
setIsFetching(true);
"use client";

try {
const apiUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/places/search?keyword=${encodeURIComponent(
query
)}`;
const response = await fetch(apiUrl, { headers: { accept: "*/*" } });

if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);

const data = await response.json();
if (data.code === 200) {
const formattedResults = data.data.map((place: Place) => ({
...place,
px: place.px ? parseFloat((place.px / 1e7).toFixed(7)) : undefined,
py: place.py ? parseFloat((place.py / 1e7).toFixed(7)) : undefined,
}));
setResults(formattedResults);
} else {
setResults([]);
alert(`장소 검색에 실패했습니다: ${data.message}`);
}
} catch (error) {
alert("서버에서 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
} finally {
setIsFetching(false);
}
},
[isFetching]
);

// fetchPlacesBySearch에 debounce 적용
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { useLocationStore } from "@/app/eventcreate-page/stores/useLocationStore";
import { LocationInputProps } from "@/app/eventcreate-page/types/types"; // Place 임포트 제거

function LocationInput({
className,
value = "",
onSelect,
}: LocationInputProps) {
const router = useRouter();
const { selectedLocation } = useLocationStore();
const [location, setLocationState] = useState(value);

// selectedLocation이 변경될 때 location 상태를 업데이트
useEffect(() => {
const debouncedFetch = debounce((query: string) => {
fetchPlacesBySearch(query);
}, 300);

if (location.length > 0) {
debouncedFetch(location);
} else {
setResults([]);
onSelect({ name: "", address: "", px: undefined, py: undefined });
}

return () => {
debouncedFetch.cancel();
};
}, [location, fetchPlacesBySearch, onSelect]); // fetchPlacesBySearch 포함

// 검색 인풋 핸들러
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocation(e.target.value);
};

// 장소 선택 핸들러
const handleSelectPlace = (place: Place) => {
setLocation(place.name);
setResults([]);
onSelect(place);
};

// 현재 위치 핸들러
const handleCurrentLocation = async () => {
if (isFetching) return;
setIsFetching(true);

if (!navigator.geolocation) {
alert("현재 위치를 지원하지 않는 브라우저입니다.");
setIsFetching(false);
return;
if (selectedLocation && selectedLocation.name !== location) {
setLocationState(selectedLocation.name);
if (onSelect) {
onSelect(selectedLocation); // selectedLocation을 상위 컴포넌트로 전달
}
}
}, [selectedLocation, onSelect, location]);

navigator.geolocation.getCurrentPosition(
({ coords }) => {
const { latitude: py, longitude: px } = coords;
const coordinatesString = `위도: ${py}, 경도: ${px}`;
setLocation(coordinatesString);

onSelect({
name: coordinatesString,
address: coordinatesString,
px,
py,
});
// input 필드의 value가 변경될 때 상태를 업데이트
useEffect(() => {
setLocationState(value);
}, [value]);

setIsFetching(false);
},
() => {
alert("위치 정보를 가져오는 중 오류가 발생했습니다.");
setIsFetching(false);
}
);
const handleLocationInputClick = () => {
router.push("/eventcreate-page/location-search");
};

return (
<div className={`relative flex flex-col ${className}`}>
<div className="text-black text-xl font-semibold leading-loose mb-[12px]">
<div className={`relative flex flex-col ${className} mt-4`}>
<div className="text-[#2c2c2c] text-xl font-semibold font-['Pretendard'] leading-loose mb-4">
어떤 공간을 찾고 계신가요?
</div>
<div className="relative w-[328px] h-14 p-4 bg-background-light rounded-lg flex items-center">
<button
type="button"
className="relative w-[328px] h-14 px-4 bg-[#f7f7f7] rounded-lg flex items-center mb-4 cursor-pointer"
onClick={handleLocationInputClick}
>
<Image
src="/images/Search.svg"
alt="돋보기 아이콘"
Expand All @@ -135,34 +54,11 @@ function LocationInput({ className, onSelect }: LocationInputProps) {
<input
type="text"
value={location}
onChange={handleSearch}
placeholder="장소를 입력해주세요"
className="bg-transparent border-none flex-grow text-base font-medium font-['Pretendard'] text-[#2c2c2c] placeholder-[#8e8e8e] outline-none"
/>
<div
role="button"
tabIndex={0}
className="w-[38px] h-[38px] bg-darkGray rounded flex items-center justify-center cursor-pointer ml-3"
onClick={handleCurrentLocation}
onKeyDown={(e) => {
if (e.key === "Enter") handleCurrentLocation();
}}
>
<Image
src="/images/Location.svg"
alt="위치 아이콘"
width={34}
height={34}
/>
</div>
</div>
{results.length > 0 && (
<SearchResults
results={results}
searchTerm={location}
onSelect={handleSelectPlace}
className="bg-transparent border-none flex-grow text-[#2c2c2c] text-base font-medium font-['Pretendard'] leading-normal outline-none"
readOnly
/>
)}
</button>
</div>
);
}
Expand Down
Loading
Loading