diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 94b86985..aff5095d 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -36,7 +36,7 @@ jobs: - name: Chromatic에 게시 id: chromatic - uses: chromaui/action@v1 # Chromatic의 GitHub Action + uses: chromaui/action@latest with: projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} # 환경 변수 참조 token: ${{ secrets.GITHUB_TOKEN }} # GitHub 인증 토큰 diff --git a/next.config.mjs b/next.config.mjs index 0310718b..d5464cbd 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,11 +1,5 @@ /** @type {import('next').NextConfig} */ -import path from "path"; -import { fileURLToPath } from "url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - const nextConfig = { reactStrictMode: true, swcMinify: true, @@ -17,20 +11,20 @@ const nextConfig = { port: "", pathname: "/Albaform/**", }, + { + protocol: "https", + hostname: "lh3.googleusercontent.com", + port: "", + pathname: "/a/**", + }, ], }, + // Webpack 커스터마이징 제거 webpack(config) { config.module.rules.push({ test: /\.svg$/, use: ["@svgr/webpack"], }); - - config.module.rules.push({ - test: /\.css$/, - use: ["style-loader", "css-loader", "postcss-loader"], - include: [path.resolve(__dirname, "node_modules/react-datepicker"), path.resolve(__dirname, "src/app")], - }); - return 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)/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 c8ecb4c9..a6591cf8 100644 --- a/src/app/(pages)/albaList/page.tsx +++ b/src/app/(pages)/albaList/page.tsx @@ -152,7 +152,11 @@ export default function AlbaList() { {page.data.map((form) => (
- + + +
))}
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/hooks/queries/form/userFormDetail.ts b/src/hooks/queries/form/userFormDetail.ts new file mode 100644 index 00000000..a1029129 --- /dev/null +++ b/src/hooks/queries/form/userFormDetail.ts @@ -0,0 +1,27 @@ +import { FormDetailResponse } from "@/types/response/form"; +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; + +interface UseUserFormDetailParams { + formId?: number; +} + +export const useUserFormDetail = ({ formId }: UseUserFormDetailParams) => { + const query = useQuery({ + queryKey: ["formDetail", formId], + queryFn: async () => { + if (!formId) { + throw new Error("formId가 없습니다."); + } + const response = await axios.get(`/api/forms/${formId}`); + return response.data; + }, + enabled: !!formId, // formId가 유효한 경우에만 쿼리를 실행 + }); + + return { + ...query, + isPending: query.isPending, + error: query.error, + }; +}; diff --git a/src/utils/oauthLoginError.ts b/src/utils/oauthLoginError.ts new file mode 100644 index 00000000..ac54f41d --- /dev/null +++ b/src/utils/oauthLoginError.ts @@ -0,0 +1,10 @@ +export class RedirectError extends Error { + redirectPath: string; + + constructor(redirectPath: string) { + super("Redirect required"); + this.redirectPath = redirectPath; + + Object.setPrototypeOf(this, RedirectError.prototype); + } +}