Skip to content

Commit

Permalink
이벤트 생성 QA 반영
Browse files Browse the repository at this point in the history
  • Loading branch information
karnelll committed Nov 3, 2024
1 parent 3f37eb9 commit b37e1a6
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 297 deletions.
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

0 comments on commit b37e1a6

Please sign in to comment.