Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default function Home() {
한 곳에서 관리하는 알바 구인 플랫폼
</p>
{user ? (
<Link href="/albaList">
<Link href="/albalist">
<p className="font-nexon-regular text-black inline-block rounded-lg bg-green-500 px-4 py-2 text-sm sm:px-6 sm:py-3 sm:text-base md:px-8 md:py-4 md:text-lg lg:px-10 lg:py-5 lg:text-xl">
알바 둘러보기
</p>
Expand Down
10 changes: 5 additions & 5 deletions src/app/(pages)/(albaform)/addform/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,19 @@ export default function AddFormPage() {

// 원하는 필드만 포함된 새로운 객체 만들기
const filteredData = Object.entries(currentValues)
.filter(([key]) => !excludedKeys.includes(key)) // 제외할 키를 필터링
.filter(([key]) => !excludedKeys.includes(key))
.reduce((acc: Partial<SubmitFormDataType>, [key, value]) => {
if (key === "numberOfPositions") {
// numberOfPositions는 숫자형으로 변환
acc[key] = Number(value);
} else if (key === "hourlyWage") {
// hourlyWage는 쉼표를 제거하고 숫자형으로 변환
if (value.includes(",")) acc[key] = Number(value.replaceAll(/,/g, "")); // 쉼표 제거 후 숫자형 변환
// 문자열이면 콤마 제거 후 숫자로 변환
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로직을 수정해주셨네요 👍

acc[key] = typeof value === "string" ? Number(value.replace(/,/g, "")) : Number(value);
} else {
acc[key as keyof SubmitFormDataType] = value; // 나머지 값은 그대로 추가
acc[key as keyof SubmitFormDataType] = value;
}
return acc;
}, {});

await axios.post("/api/forms", filteredData);
},
onSuccess: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { cn } from "@/lib/tailwindUtil";
import { useFormContext } from "react-hook-form";
import { useEffect, useState } from "react";
import Label from "../../component/Label";
import { SubmitFormDataType } from "@/types/addform";
import Image from "next/image";

// 알바폼 만들기 - 사장님 - 1-모집내용

Expand All @@ -24,28 +26,51 @@ export default function RecruitContentSection() {

const currentValue = getValues();

// 임시저장 데이터 불러오기
Copy link
Collaborator

@hongggyelim hongggyelim Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

임시저장이 있을때 -> 모달로
< 임시저장 데이터가 있습니다. 이어서 작성하시겠습니까? > 띄우고 데이터 불러오는 로직이 필요합니다.
그게 아니라면 빈 값으로 로딩 되어야합니다 !
추후 구현 예정이었는데 추가를 해주셨네요~

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

작성해주신 브랜치에서 테스트해보니 지금은 addform 페이지 들어가면 바로 임시 저장한 데이터가 불러와져서요~

useEffect(() => {
const tempData = localStorage.getItem("tempAddFormData");
if (tempData) {
const parsedData: SubmitFormDataType = JSON.parse(tempData);

// 기본 필드 설정
if (parsedData.title) setValue("title", parsedData.title);
if (parsedData.description) setValue("description", parsedData.description);

// 모집 기간 설정
if (parsedData.recruitmentStartDate && parsedData.recruitmentEndDate) {
const startDate = new Date(parsedData.recruitmentStartDate);
const endDate = new Date(parsedData.recruitmentEndDate);
setRecruitmentDateRange([startDate, endDate]);
setValue("recruitmentStartDate", startDate.toISOString());
setValue("recruitmentEndDate", endDate.toISOString());
}

// 이미지 URL 설정
if (parsedData.imageUrls?.length > 0) {
setValue("imageUrls", parsedData.imageUrls);
// URL을 직접 사용하여 이미지 리스트 생성
const savedImageList = parsedData.imageUrls.map((url: string) => ({
file: new File([], url.split("/").pop() || "saved-image"),
url,
id: crypto.randomUUID(),
}));
setInitialImageList(savedImageList);
}
}
}, [setValue]);

// 이미지 파일 change핸들러
const handleChangeImages = (files: File[]) => {
// 훅폼 데이터에 추가-> 상위 페이지에서 "imageFiles" data를 관리할 수 있음
setValue("imageFiles", files);

// 기존 이미지 리스트와 새로운 이미지를 합침
setInitialImageList((prevList) => [
...prevList,
...files.map((file: File) => ({
file,
url: URL.createObjectURL(file),
id: crypto.randomUUID(),
})),
]);
};
const newImages = files.map((file: File) => ({
file,
url: URL.createObjectURL(file),
id: crypto.randomUUID(),
}));

// 컴포넌트가 마운트될 때 이미지 초기값 설정 (초기로딩 제외)
useEffect(() => {
if (currentValue.imageFiles?.length > 0) {
handleChangeImages(currentValue.imageFiles);
}
}, []);
setInitialImageList(newImages); // 새 이미지로 완전히 교체
};

// 날짜 선택
const [recruitmentDateRange, setRecruitmentDateRange] = useState<[Date | null, Date | null]>([null, null]);
Expand Down Expand Up @@ -98,14 +123,16 @@ export default function RecruitContentSection() {
</div>

<Label>이미지 첨부</Label>
<ImageInput
{...register("imageUrls", { required: "이미지는 필수입니다." })}
onChange={(files: File[]) => {
handleChangeImages(files);
}}
initialImageList={initialImageList || []}
/>
{errors.imageUrls && <p className={cn(errorTextStyle, "")}>{errors.imageUrls.message as string}</p>}
<div>
<ImageInput
{...register("imageUrls", { required: "이미지는 필수입니다." })}
onChange={(files: File[]) => {
handleChangeImages(files);
}}
initialImageList={initialImageList}
/>
{errors.imageUrls && <p className={cn(errorTextStyle, "")}>{errors.imageUrls.message as string}</p>}
</div>
</form>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
"use client";

import { useFormContext } from "react-hook-form";
import { useState, ChangeEvent, MouseEvent, useEffect } from "react";
import { useState, ChangeEvent, MouseEvent, useEffect, useCallback } from "react";
import { cn } from "@/lib/tailwindUtil";
import DatePickerInput from "@/app/components/input/dateTimeDaypicker/DatePickerInput";
import LocationInput from "@/app/components/input/text/LocationInput";
import TimePickerInput from "@/app/components/input/dateTimeDaypicker/TimePickerInput";
import DayPickerList from "@/app/components/input/dateTimeDaypicker/DayPickerList";
import BaseInput from "@/app/components/input/text/BaseInput";
import CheckBtn from "@/app/components/button/default/CheckBtn";
import formatMoney from "@/utils/formatMoney";
import Label from "../../component/Label";
import Script from "next/script";
import LocationInput from "@/app/components/input/text/LocationInput";

// 알바폼 만들기 - 사장님 - 3-근무조건
export default function WorkConditionSection() {
Expand Down Expand Up @@ -61,19 +62,47 @@ export default function WorkConditionSection() {
useEffect(() => {
const selectedDays = getValues("workDays") || [];
setSelectedWorkDays(selectedDays);
const wage = getValues("hourlyWage") || 0;
setDisplayWage(wage);
}, [getValues]);
const wage = getValues("hourlyWage");
// wage가 숫자인 경우에만 formatMoney 적용
setDisplayWage(wage ? formatMoney(wage.toString()) : "");

// 로컬 스토리지에서 주소 데이터 가져오기
const tempData = localStorage.getItem("tempAddFormData");
if (tempData) {
const { location } = JSON.parse(tempData);
if (location) {
setValue("location", location);
trigger("location");
}
}
}, [getValues, setValue, trigger]);

const errorTextStyle =
"absolute -bottom-[26px] right-1 text-[13px] text-sm font-medium leading-[22px] text-state-error lg:text-base lg:leading-[26px]";

// 주소 변경 핸들러만 유지
const handleAddressChange = useCallback(
(fullAddress: string) => {
setValue("location", fullAddress);
trigger("location");
},
[setValue, trigger]
);

return (
<div className="relative">
<Script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js" strategy="afterInteractive" />

<form className="my-8 flex flex-col gap-4">
{/* 지도 API 연동 */}
<Label>근무 위치</Label>
<LocationInput variant="white" {...register("location", { required: "근무 위치를 작성해주세요." })} />
<div className="relative">
<LocationInput
onAddressChange={handleAddressChange}
errormessage={errors.location?.message as string}
variant="white"
value={watch("location")}
/>
</div>

<div className="relative flex flex-col gap-2">
<Label>근무 기간</Label>
Expand Down Expand Up @@ -137,7 +166,7 @@ export default function WorkConditionSection() {
required: "시급을 작성해주세요.",
})}
value={displayWage}
// type = "string" -> 폼데이터에는 숫자형으로, 화면에는 세자리 콤마 추가
// type = "string" -> 폼데이터에는 자형으로, 화면에는 세자리 콤마 추가
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const numericValue = Number(value.replace(/,/g, ""));
Expand Down
4 changes: 3 additions & 1 deletion src/app/(pages)/myAlbaform/(role)/owner/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,9 @@ export default function AlbaList() {
<React.Fragment key={page.nextCursor}>
{page.data.map((form) => (
<div key={form.id}>
<AlbaListItem {...form} />
<Link href={`/alba/${form.id}`}>
<AlbaListItem {...form} />
</Link>
</div>
))}
</React.Fragment>
Expand Down
23 changes: 15 additions & 8 deletions src/app/components/card/cardList/AlbaListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,17 @@ const AlbaListItem = ({
};
}, [showDropdown]);

// 지원하기
const handleFormApplication = () => {
// 드롭다운 토글 핸들러 수정
const handleDropdownToggle = (e: React.MouseEvent) => {
e.preventDefault(); // Link 클릭 방지
e.stopPropagation(); // 이벤트 전파 방지
setShowDropdown(!showDropdown);
};

// 드롭다운 메뉴 아이템 클릭 핸들러 수정
const handleFormApplication = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setShowDropdown(false);
openModal("customForm", {
isOpen: true,
Expand Down Expand Up @@ -85,8 +94,9 @@ const AlbaListItem = ({
});
};

// 스크랩
const handleFormScrap = () => {
const handleFormScrap = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setShowDropdown(false);
openModal("customForm", {
isOpen: true,
Expand Down Expand Up @@ -172,10 +182,7 @@ const AlbaListItem = ({
</div>
{/* 케밥 메뉴 */}
<div ref={dropdownRef} className="relative">
<button
className="hover:text-grayscale-700 text-grayscale-500"
onClick={() => setShowDropdown(!showDropdown)}
>
<button className="hover:text-grayscale-700 text-grayscale-500" onClick={handleDropdownToggle}>
<BsThreeDotsVertical className="h-6 w-6" />
</button>
{/* 드롭다운 메뉴 */}
Expand Down
16 changes: 11 additions & 5 deletions src/app/components/input/file/ImageInput/ImageInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import { HiUpload } from "react-icons/hi";
import { forwardRef, useState } from "react";
import { forwardRef, useState, useEffect } from "react";
import { toast } from "react-hot-toast";
import PreviewItem from "./PreviewItem";
import { cn } from "@/lib/tailwindUtil";
Expand All @@ -17,7 +17,13 @@ interface ImageInputProps {
}

const ImageInput = forwardRef<HTMLInputElement, ImageInputProps>((props, ref) => {
const [imageList, setImageList] = useState<ImageInputType[]>([]); // 단순히 이미지 프리뷰를 위한 상태 관리
const [imageList, setImageList] = useState<ImageInputType[]>(props.initialImageList || []);

useEffect(() => {
if (props.initialImageList?.length > 0) {
setImageList(props.initialImageList);
}
}, [props.initialImageList]);

const handleFileChange = (selectedFile: File | null) => {
if (selectedFile) {
Expand All @@ -39,11 +45,11 @@ const ImageInput = forwardRef<HTMLInputElement, ImageInputProps>((props, ref) =>
];

setImageList(newImageList);
props.onChange?.(newImageList.map((img) => img.file).filter((file) => file !== null));
props.onChange?.(newImageList.map((img) => img.file).filter((file) => file !== null) as File[]);
}
};

const handleOpenFileSelecter = () => {
const handleOpenFileSelector = () => {
if (typeof ref === "function") {
// input 요소를 찾아서 클릭
const fileInput = document.querySelector(`input[name="${props.name}"]`);
Expand Down Expand Up @@ -76,7 +82,7 @@ const ImageInput = forwardRef<HTMLInputElement, ImageInputProps>((props, ref) =>
// 인풋 + 프리뷰 wrapper
<div className="flex gap-5 lg:gap-6">
<div
onClick={handleOpenFileSelecter}
onClick={handleOpenFileSelector}
className={cn(
"relative size-20 cursor-pointer rounded-lg lg:size-[116px]",
colorStyle.bgColor,
Expand Down
64 changes: 51 additions & 13 deletions src/app/components/input/text/LocationInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,59 @@
import { IoLocationSharp } from "react-icons/io5";
import BaseInput from "./BaseInput";
import { BaseInputProps } from "@/types/textInput";
import { forwardRef } from "react";
import { forwardRef, useCallback, useState, useEffect } from "react";
import Script from "next/script";

interface LocationInputProps extends BaseInputProps {
onAddressChange?: (fullAddress: string) => void;
readOnly?: boolean;
value?: string;
}

const LocationInput = forwardRef<HTMLInputElement, LocationInputProps>(
(
{ type = "text", variant, errormessage, feedbackMessage, onAddressChange, readOnly = true, value, ...props },
ref
) => {
const [address, setAddress] = useState("");

useEffect(() => {
if (value) {
setAddress(value);
}
}, [value]);

const handleOpenPostcode = useCallback(() => {
if (typeof window === "undefined" || !window.daum) return;

new window.daum.Postcode({
oncomplete: (data) => {
const newAddress = data.address;
setAddress(newAddress);
onAddressChange?.(newAddress);
},
}).open();
}, [onAddressChange]);

const LocationInput = forwardRef<HTMLInputElement, BaseInputProps>(
({ type = "text", variant, errormessage, feedbackMessage, ...props }, ref) => {
return (
<BaseInput
ref={ref}
type={type}
variant={variant || "white"}
beforeIcon={<IoLocationSharp className="size-5 text-grayscale-100 lg:size-8" />}
placeholder="위치를 입력해주세요."
errormessage={errormessage}
feedbackMessage={feedbackMessage}
{...props}
/>
<>
<Script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js" strategy="afterInteractive" />
<div className="relative w-full">
<BaseInput
ref={ref}
type={type}
variant={variant || "white"}
beforeIcon={<IoLocationSharp className="size-5 text-grayscale-100 lg:size-8" />}
placeholder="클릭하여 주소를 검색하세요"
value={address}
readOnly={readOnly}
errormessage={errormessage}
feedbackMessage={feedbackMessage}
onClick={handleOpenPostcode}
{...props}
/>
</div>
</>
);
}
);
Expand Down
Loading