diff --git a/next.config.mjs b/next.config.mjs index 21b3b2ec..5b449033 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -11,6 +11,12 @@ const nextConfig = { port: "", pathname: "/Albaform/**", }, + { + protocol: "https", + hostname: "lh3.googleusercontent.com", + port: "", + pathname: "/a/**", + }, ], }, webpack(config) { diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 13c4d836..c215565b 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -63,6 +63,11 @@ export default function LoginPage() { {isPending ? "로그인 중..." : "로그인"} +
+
+ SNS 계정으로 로그인하기 +
+
+
+
+ SNS 계정으로 회원가입하기 +
+
+
+
+ SNS 계정으로 회원가입하기 +
+
()); const observer = useRef(null); + const { user } = useUser(); useEffect(() => { // 클라이언트 환경에서만 IntersectionObserver 실행 @@ -57,11 +59,19 @@ export default function Home() {

한 곳에서 관리하는 알바 구인 플랫폼

- -

- 알바를 시작하기 -

- + {user ? ( + +

+ 알바 둘러보기 +

+ + ) : ( + +

+ 알바를 시작하기 +

+ + )}
{/* 배경 이미지 */} diff --git a/src/app/(pages)/(albaform)/addform/RecruitCondition.tsx b/src/app/(pages)/(albaform)/addform/RecruitConditionSection.tsx similarity index 97% rename from src/app/(pages)/(albaform)/addform/RecruitCondition.tsx rename to src/app/(pages)/(albaform)/addform/RecruitConditionSection.tsx index 4678d3a2..29f5dd5b 100644 --- a/src/app/(pages)/(albaform)/addform/RecruitCondition.tsx +++ b/src/app/(pages)/(albaform)/addform/RecruitConditionSection.tsx @@ -4,7 +4,7 @@ import Label from "../Label"; import InputDropdown from "@/app/components/button/dropdown/InputDropdown"; // 알바폼 만들기 - 사장님- 2-모집조건 -export default function RecruitCondition() { +export default function RecruitConditionSection() { const { register, formState: { errors }, diff --git a/src/app/(pages)/(albaform)/addform/RecruitContent.tsx b/src/app/(pages)/(albaform)/addform/RecruitContentSection.tsx similarity index 92% rename from src/app/(pages)/(albaform)/addform/RecruitContent.tsx rename to src/app/(pages)/(albaform)/addform/RecruitContentSection.tsx index 9f02d4e0..f1f7ee33 100644 --- a/src/app/(pages)/(albaform)/addform/RecruitContent.tsx +++ b/src/app/(pages)/(albaform)/addform/RecruitContentSection.tsx @@ -7,11 +7,10 @@ import DatePickerInput from "@/app/components/input/dateTimeDaypicker/DatePicker import { cn } from "@/lib/tailwindUtil"; import { useFormContext } from "react-hook-form"; import { useEffect, useState } from "react"; -import { useSearchParams } from "next/navigation"; // 알바폼 만들기 - 사장님 - 1-모집내용 -export default function RecruitContent() { +export default function RecruitContentSection() { // 이미지 파일을 로컬 상태에 저장 const [initialImageList, setInitialImageList] = useState<{ file: File; url: string; id: string }[]>([]); @@ -41,12 +40,9 @@ export default function RecruitContent() { ]); }; - const searchParams = useSearchParams(); - const currentParam = searchParams.get("tab"); - const initialLoad = currentParam === null; // 초기 로딩 여부 확인 // 컴포넌트가 마운트될 때 이미지 초기값 설정 (초기로딩 제외) useEffect(() => { - if (!initialLoad && currentValue.imageFiles?.length > 0) { + if (currentValue.imageFiles?.length > 0) { handleChangeImages(currentValue.imageFiles); } }, []); diff --git a/src/app/(pages)/(albaform)/addform/WorkCondition.tsx b/src/app/(pages)/(albaform)/addform/WorkConditionSection.tsx similarity index 99% rename from src/app/(pages)/(albaform)/addform/WorkCondition.tsx rename to src/app/(pages)/(albaform)/addform/WorkConditionSection.tsx index ce3e957e..669e8720 100644 --- a/src/app/(pages)/(albaform)/addform/WorkCondition.tsx +++ b/src/app/(pages)/(albaform)/addform/WorkConditionSection.tsx @@ -12,7 +12,7 @@ import BaseInput from "@/app/components/input/text/BaseInput"; import CheckBtn from "@/app/components/button/default/CheckBtn"; // 알바폼 만들기 - 사장님 - 3-근무조건 -export default function WorkCondition() { +export default function WorkConditionSection() { const { register, setValue, diff --git a/src/app/(pages)/(albaform)/addform/page.tsx b/src/app/(pages)/(albaform)/addform/page.tsx index 24254bac..93b1a3bb 100644 --- a/src/app/(pages)/(albaform)/addform/page.tsx +++ b/src/app/(pages)/(albaform)/addform/page.tsx @@ -4,13 +4,13 @@ import { useRouter, useSearchParams } from "next/navigation"; import { FormProvider, useForm } from "react-hook-form"; import axios from "axios"; import TabMenuDropdown from "@/app/components/button/dropdown/TabMenuDropdown"; -import RecruitCondition from "./RecruitCondition"; import Button from "@/app/components/button/default/Button"; import { toast } from "react-hot-toast"; import { useMutation } from "@tanstack/react-query"; import { useUpdateProfile } from "@/hooks/queries/user/me/useUpdateProfile"; -import RecruitContent from "./RecruitContent"; -import WorkCondition from "./WorkCondition"; +import RecruitContentSection from "./RecruitContentSection"; +import RecruitConditionSection from "./RecruitConditionSection"; +import WorkConditionSection from "./WorkConditionSection"; interface SubmitFormDataType { isPublic: boolean; @@ -66,13 +66,12 @@ export default function AddFormPage() { const { setValue, - getValues, handleSubmit, formState: { isDirty, isValid }, } = methods; // 훅폼에서 관리하는 전체 데이터를 가져오는 함수 - const currentValues: SubmitFormDataType = getValues(); + const currentValues: SubmitFormDataType = methods.watch(); // 이미지 업로드 api 처리를 위해 별도 변수에 할당 const imageFiles = currentValues.imageFiles; @@ -135,17 +134,17 @@ export default function AddFormPage() { "모집 조건": "recruit-condition", "근무 조건": "work-condition", }[option]; - router.replace(`/addform?tab=${params}`); + router.push(`/addform?tab=${params}`); }; const renderChildren = () => { switch (selectedOption) { case "모집 내용": - return ; + return ; case "모집 조건": - return ; + return ; case "근무 조건": - return ; + return ; default: return <>; } @@ -204,33 +203,27 @@ export default function AddFormPage() { window.localStorage.setItem("tempAddFormData", JSON.stringify(currentValues)); } toast.success("임시 저장되었습니다."); - // console.log("임시저장 데이터", currentValues); + console.log("임시저장 데이터", currentValues); }; // 각각의 탭 작성중 여부 const isEditingRecruitContent = - currentValues.title !== "" || - currentValues.description !== "" || - currentValues.recruitmentStartDate !== "" || - currentValues.imageUrls + currentValues.title !== "" || currentValues.description !== "" || currentValues.recruitmentStartDate !== undefined ? true : false; const isEditingRecruitCondition = - currentValues.gender || - currentValues.numberOfPositions || - currentValues.education || - currentValues.age || - currentValues.preferred + currentValues.gender !== "" || + currentValues.numberOfPositions !== 0 || + currentValues.education !== "" || + currentValues.age !== "" || + currentValues.preferred !== "" ? true : false; const isEditingWorkCondition = - currentValues.location || - currentValues.workDays || - currentValues.workStartTime || - currentValues.workStartDate || - currentValues.hourlyWage || - currentValues.isNegotiableWorkDays || - currentValues.isPublic + currentValues.location !== "" || + currentValues.workStartTime !== "" || + currentValues.workStartDate !== "" || + currentValues.hourlyWage > 0 ? true : false; @@ -242,7 +235,7 @@ export default function AddFormPage() { options={[ { label: "모집 내용", - isEditing: isEditingRecruitContent || initialLoad || currentParam === "recruit-condition", + isEditing: isEditingRecruitContent || initialLoad || currentParam === "recruit-content", }, { label: "모집 조건", isEditing: isEditingRecruitCondition || currentParam === "recruit-condition" }, { label: "근무 조건", isEditing: isEditingWorkCondition || currentParam === "work-condition" }, diff --git a/src/app/(pages)/albaFormDetail/applicant/[formId]/page.tsx b/src/app/(pages)/albaFormDetail/applicant/[formId]/page.tsx new file mode 100644 index 00000000..29a68e2a --- /dev/null +++ b/src/app/(pages)/albaFormDetail/applicant/[formId]/page.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { useUserFormDetail } from "@/hooks/queries/form/userFormDetail"; +import React, { useEffect, useState } from "react"; +import CardChipIcon from "@/app/components/card/cardList/CardChipIcon"; +import Chip from "@/app/components/chip/Chip"; + +export default function AlbaFormDetailPage() { + const { formId } = useParams(); // useParams로 formId 추출 + const [formIdState, setFormIdState] = useState(0); + + useEffect(() => { + // formId가 문자열로 전달되므로 숫자로 변환하여 상태에 저장 + if (formId) { + setFormIdState(Number(formId)); // formId를 숫자로 변환하여 상태에 저장 + } + }, [formId]); + + // formId가 설정되면 useUserFormDetail 호출 + const { data, isLoading, error } = useUserFormDetail({ formId: formIdState }); + + if (isLoading) { + return
Loading...
; + } + + if (error) { + return
Error: 데이터를 불러오는데 문제가 발생했습니다.
; + } + + if (!data) { + return
데이터가 없습니다.
; + } + + // 모집 상태 계산 + const recruitmentStatus = new Date(data.recruitmentEndDate) > new Date() ? "모집중" : "모집완료"; + + return ( +
+
사진영역
+
+
+
+ + +

{new Date(data.createdAt).toLocaleString()} 등록

+
+
+ {data.storeName || "가게명"} + + {data.location || "위치"} ・ {"경력 정보 없음"} + +
+

{data.title}

+ +
{data.description}
+
+
+
+ ); +} diff --git a/src/app/(pages)/albaFormDetail/layout.tsx b/src/app/(pages)/albaFormDetail/layout.tsx new file mode 100644 index 00000000..af80a7bd --- /dev/null +++ b/src/app/(pages)/albaFormDetail/layout.tsx @@ -0,0 +1,17 @@ +import React, { Suspense } from "react"; + +export default function AlbaFormDetailLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
로딩 중...
+
+ } + > + {children} + + + ); +} diff --git a/src/app/(pages)/albaFormDetail/owner/[id]/page.tsx b/src/app/(pages)/albaFormDetail/owner/[id]/page.tsx new file mode 100644 index 00000000..8754305e --- /dev/null +++ b/src/app/(pages)/albaFormDetail/owner/[id]/page.tsx @@ -0,0 +1,49 @@ +"use client"; +import CardChipIcon from "@/app/components/card/cardList/CardChipIcon"; +import Chip from "@/app/components/chip/Chip"; +import ChipWithIcon from "@/app/components/chip/ChipWithIcon"; +import React from "react"; + +// 알바폼 상세 사장님 페이지 +export default function AlbaFormDetailPage({ formId }: { formId: number }) { + return ( +
+
사진영역
+
+
+
+ + +

2024. 05. 04 12:30:54 등록

+
+
+ 코드잇 + 서울 종로구 ・ 경력 무관 +
+

코드잇 스터디카페 관리 (주말 오전) 모집합니다 서울 종로구 용산구 서대문

+ +
+ 코드잇 스터디 카페입니다. 주말 토, 일 오픈업무 하실 분 구합니다. 성실하게 일하실 분들만 지원 바랍니다. + 작성한 이력서(사진 부착)를 알바폼에 첨부해주시고, 아래와 같이 문자 보내주세요. 근무 중 전화통화 불가합니다. + 예) OOO입니다. __에 거주합니다. 알바폼 지원. 이력서 검토 후 면접진행자에 한해 면접일정 개별 + 연락드리겠습니다. 많은 지원 바랍니다. +
+
+
{/* 오른쪽 콘텐츠 */}
+
+
+ ); +} diff --git a/src/app/(pages)/albaFormSupportDetail/layout.tsx b/src/app/(pages)/albaFormSupportDetail/layout.tsx new file mode 100644 index 00000000..af80a7bd --- /dev/null +++ b/src/app/(pages)/albaFormSupportDetail/layout.tsx @@ -0,0 +1,17 @@ +import React, { Suspense } from "react"; + +export default function AlbaFormDetailLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
로딩 중...
+
+ } + > + {children} + + + ); +} diff --git a/src/app/(pages)/albaFormSupportDetail/page.tsx b/src/app/(pages)/albaFormSupportDetail/page.tsx new file mode 100644 index 00000000..c5df0666 --- /dev/null +++ b/src/app/(pages)/albaFormSupportDetail/page.tsx @@ -0,0 +1,44 @@ +import CardChipIcon from "@/app/components/card/cardList/CardChipIcon"; +import Chip from "@/app/components/chip/Chip"; +import React from "react"; + +// 알바폼 지원내역 상세 페이지 +export default function AlbaFormSupportDetailPage({ formId }: { formId: number }) { + return ( +
+
지도영역
+
+
+ + +

2024. 05. 04 12:30:54 등록

+
+
+ 코드잇 + 서울 종로구 ・ 경력 무관 +
+

코드잇 스터디카페 관리 (주말 오전) 모집합니다 서울 종로구 용산구 서대문

+ +
+ 코드잇 스터디 카페입니다. 주말 토, 일 오픈업무 하실 분 구합니다. 성실하게 일하실 분들만 지원 바랍니다. 작성한 + 이력서(사진 부착)를 알바폼에 첨부해주시고, 아래와 같이 문자 보내주세요. 근무 중 전화통화 불가합니다. 예) + OOO입니다. __에 거주합니다. 알바폼 지원. 이력서 검토 후 면접진행자에 한해 면접일정 개별 연락드리겠습니다. 많은 + 지원 바랍니다. +
+
+
+ ); +} diff --git a/src/app/(pages)/albaList/page.tsx b/src/app/(pages)/albaList/page.tsx index f7c74286..db73d122 100644 --- a/src/app/(pages)/albaList/page.tsx +++ b/src/app/(pages)/albaList/page.tsx @@ -150,7 +150,11 @@ export default function AlbaList() { {page.data.map((form) => (
- + + +
))}
diff --git a/src/app/api/forms/[formId]/applications/route.ts b/src/app/api/forms/[formId]/applications/route.ts index 53c274aa..733d5fd5 100644 --- a/src/app/api/forms/[formId]/applications/route.ts +++ b/src/app/api/forms/[formId]/applications/route.ts @@ -20,9 +20,10 @@ export async function POST(req: NextRequest, { params }: { params: { formId: str "Content-Type": "application/json", }, }); - + console.log(response.data); return NextResponse.json(response.data); } catch (error: unknown) { + console.error(error); if (error instanceof AxiosError) { console.error(`POST /api/forms/${params.formId}/applications error:`, error); if (error.response) { diff --git a/src/app/api/oauth/callback/google/route.ts b/src/app/api/oauth/callback/google/route.ts index 64d21043..809dd5e9 100644 --- a/src/app/api/oauth/callback/google/route.ts +++ b/src/app/api/oauth/callback/google/route.ts @@ -4,6 +4,7 @@ import { decodeJwt } from "@/middleware"; import apiClient from "@/lib/apiClient"; import { OauthLoginUser, OauthResponse, OauthSignupUser } from "@/types/oauth/oauth"; import { cookies } from "next/headers"; +import { RedirectError } from "@/utils/oauthLoginError"; export const GET = async (request: NextRequest) => { const searchParams = request.nextUrl.searchParams; @@ -82,15 +83,25 @@ export const GET = async (request: NextRequest) => { }; const loginUser = async () => { - const { data: loginResponse } = await axios.post( - `${process.env.NEXT_PUBLIC_DOMAIN_URL}/api/oauth/login/${provider}`, - googleUser.login - ); - console.log("구글 로그인 성공:", loginResponse); + try { + const { data: loginResponse } = await axios.post( + `${process.env.NEXT_PUBLIC_DOMAIN_URL}/api/oauth/login/${provider}`, + googleUser.login + ); + console.log("구글 로그인 성공:", loginResponse); - // 쿠키 저장 - const { accessToken, refreshToken } = loginResponse; - setCookies(accessToken, refreshToken); + // 쿠키 저장 + const { accessToken, refreshToken } = loginResponse; + setCookies(accessToken, refreshToken); + } catch (error: any) { + if (error.response?.status === 403) { + console.log("회원가입이 필요합니다. 회원가입 페이지로 이동합니다..."); + throw new RedirectError("/signup"); + } else { + console.error("구글 로그인 중 오류:", error.message || error); + throw new Error("로그인 중 서버 오류"); + } + } }; const setCookies = (accessToken: string, refreshToken: string) => { @@ -108,11 +119,18 @@ export const GET = async (request: NextRequest) => { }); }; - await processUser(); + try { + await processUser(); + } catch (error) { + if (error instanceof RedirectError) { + return NextResponse.redirect(new URL(error.redirectPath, request.url)); + } + throw error; + } } catch (error: any) { console.error("OAuth 처리 중 오류:", error.message || error); return NextResponse.json({ message: error.message || "서버 오류" }, { status: 500 }); } - return NextResponse.redirect(new URL("/", request.url)); + return NextResponse.redirect(new URL("/mypage", request.url)); }; diff --git a/src/app/api/oauth/callback/kakao/route.ts b/src/app/api/oauth/callback/kakao/route.ts index bc7a322a..50a4ba0d 100644 --- a/src/app/api/oauth/callback/kakao/route.ts +++ b/src/app/api/oauth/callback/kakao/route.ts @@ -3,6 +3,7 @@ import axios from "axios"; import apiClient from "@/lib/apiClient"; import { OauthLoginUser, OauthResponse, OauthSignupUser } from "@/types/oauth/oauth"; import { cookies } from "next/headers"; +import { RedirectError } from "@/utils/oauthLoginError"; export const GET = async (request: NextRequest) => { const searchParams = request.nextUrl.searchParams; @@ -71,8 +72,13 @@ export const GET = async (request: NextRequest) => { const { accessToken, refreshToken } = loginResponse; setCookies(accessToken, refreshToken); } catch (error: any) { - console.error("카카오 로그인 중 오류:", error.message || error); - throw new Error("로그인 중 서버 오류"); + if (error.response?.status === 403) { + console.log("회원가입이 필요합니다. 회원가입 시도 중..."); + //회원가입 페이지로 리다이렉트 + throw new RedirectError("/signup"); + } else { + throw new Error("회원가입 중 서버 오류"); + } } }; @@ -98,5 +104,5 @@ export const GET = async (request: NextRequest) => { return NextResponse.json({ message: error.message || "서버 오류" }, { status: 500 }); } - return NextResponse.redirect(new URL("/", request.url)); + return NextResponse.redirect(new URL("/mypage", request.url)); }; diff --git a/src/app/components/input/file/ImageInput/ImageInput.tsx b/src/app/components/input/file/ImageInput/ImageInput.tsx index c80f7f60..f73f737e 100644 --- a/src/app/components/input/file/ImageInput/ImageInput.tsx +++ b/src/app/components/input/file/ImageInput/ImageInput.tsx @@ -17,7 +17,7 @@ interface ImageInputProps { } const ImageInput = forwardRef((props, ref) => { - const [imageList, setImageList] = useState(props.initialImageList || []); // 단순히 이미지 프리뷰를 위한 상태 관리 + const [imageList, setImageList] = useState([]); // 단순히 이미지 프리뷰를 위한 상태 관리 const handleFileChange = (selectedFile: File | null) => { if (selectedFile) { @@ -42,6 +42,7 @@ const ImageInput = forwardRef((props, ref) => props.onChange?.(newImageList.map((img) => img.file).filter((file) => file !== null)); } }; + const handleOpenFileSelecter = () => { if (typeof ref === "function") { // input 요소를 찾아서 클릭 @@ -55,6 +56,10 @@ const ImageInput = forwardRef((props, ref) => }; const handleDeleteImage = (targetId: string) => { + const targetImage = imageList.find((image) => image.id === targetId); + if (targetImage) { + URL.revokeObjectURL(targetImage.url); // URL 객체 해제 + } const newImageList = imageList.filter((image) => image.id !== targetId); setImageList(newImageList); props.onChange?.(newImageList.map((img) => img.file).filter((file) => file !== null)); diff --git a/src/app/components/layout/addFormLayout/ApplyHeader.tsx b/src/app/components/layout/addFormLayout/ApplyHeader.tsx index 11ecc82e..460a9107 100644 --- a/src/app/components/layout/addFormLayout/ApplyHeader.tsx +++ b/src/app/components/layout/addFormLayout/ApplyHeader.tsx @@ -2,7 +2,7 @@ import Button from "@/app/components/button/default/Button"; const ApplyHeader = ({ title, onCancel }: { title: string; onCancel: () => void }) => { return ( -
+

{title}