Skip to content
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
19 changes: 18 additions & 1 deletion 8-sprint-fe/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "sprint-fe-project.s3.ap-northeast-2.amazonaws.com",
},
{
protocol: "https",
hostname: "st.depositphotos.com",
},
{
protocol: "https",
hostname: "i.pinimg.com",
},
],
},
};

export default nextConfig;
3 changes: 3 additions & 0 deletions 8-sprint-fe/public/btn_visibility_off_24px.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions 8-sprint-fe/public/btn_visibility_on_24px.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions 8-sprint-fe/public/ic_google.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions 8-sprint-fe/public/ic_kakao.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions 8-sprint-fe/src/api/fetchClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const URL = `${process.env.NEXT_PUBLIC_CODEIT_URL}`;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

제 환경에서는 env 파일이 없어서 http://localhost:3000/undefined/auth/signIn 경로로 요청이 가서 정상적인 확인이 어렵네요 ㅠ

env 파일 자체는 레포에 올리지 않는 것은 맞습니다. 따라서 실무에서는 env.example 형태로 예시 파일을 추가하고 실제 env 정보는 DM으로 전달하거나 따로 문서로 관리하는 편입니다.

참고해주시면 좋겠습니다~


// 회원가입
export async function fetchSignUp({
email,
nickname,
password,
passwordConfirmation,
}) {
const response = await fetch(`${URL}/auth/signUp`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
cache: "force-cache",
body: JSON.stringify({
email,
nickname,
password,
passwordConfirmation,
}),
});

if (!response.ok) {
const errorText = await response.text(); // JSON이 아닐 수도 있으니 text로
throw new Error(errorText);
}

return await response.json();
}

// 로그인
export async function fetchLogin({ email, password }) {
const response = await fetch(`${URL}/auth/signIn`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
cache: "force-cache",
body: JSON.stringify({
email,
password,
}),
});

if (!response.ok) {
const errorData = await response.json();
throw new Error("로그인에 실패했습니다.");
}

return await response.json();
}

// get me
export async function fetchGetMe(token) {
const response = await fetch(`${URL}/users/me`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});

if (!response.ok) {
const errorData = await response.json();
throw new Error("로그인에 실패했습니다.");
}

return await response.json();
}
25 changes: 25 additions & 0 deletions 8-sprint-fe/src/api/fetchProducts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const URL = `${process.env.NEXT_PUBLIC_CODEIT_URL}/products`;

// products list get
export const fetchProducts = async ({
page = 1,
pageSize = 10,
orderBy = "recent",
}) => {
const response = await fetch(
`${URL}?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}`
);
if (!response.ok) {
throw new Error("서버에서 데이터를 가져오는데 실패했습니다.");
}
return await response.json();
};

// product get
export const fetchProduct = async ({ id }) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

용도별로 fetch를 따로 나눈 점이 좋습니다 👍

const response = await fetch(`${URL}/${id}`);
if (!response.ok) {
throw new Error("서버에서 데이터를 가져오는데 실패했습니다.");
}
return await response.json();
};
50 changes: 50 additions & 0 deletions 8-sprint-fe/src/api/userService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { cookieFetch } from "@/lib/fetchClient";

// FormData 전용 fetch 함수 (Content-Type 헤더 없음)
const formDataFetch = async (url, options = {}) => {
const baseURL = process.env.NEXT_PUBLIC_API_URL;
const defaultOptions = {
// 쿠키 전송을 위한 설정
credentials: "include",
// 서버 컴포넌트에서도 매번 재검증
cache: "no-store",
};

const mergedOptions = {
...defaultOptions,
...options,
};

const response = await fetch(`${baseURL}${url}`, mergedOptions);

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

try {
return await response.json();
} catch (e) {
return { status: response.status, ok: response.ok };
}
};

export const userService = {
// 사용자 정보 요청
getMe: () => cookieFetch("/users/me"),

// 사용자 링크 요청
getMyLinks: () => cookieFetch("/users/me/links"),

// 사용자 정보 업데이트 (multipart/form-data)
updateMe: (formData) =>
formDataFetch("/users/me", {
method: "PATCH",
body: formData,
}),

// 링크 삭제
deleteLink: (linkId) =>
cookieFetch(`/users/me/links/${linkId}`, {
method: "DELETE",
}),
};
108 changes: 108 additions & 0 deletions 8-sprint-fe/src/app/(auth)/_components/LoginPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"use client";
import { AuthLogo } from "@/app/(components)/atoms/Logo";
import OAuth from "@/app/(components)/atoms/OAuth";
import PasswordInput from "@/app/(components)/atoms/PasswordInput";
import TextInput from "@/app/(components)/atoms/TextInput";
import Link from "next/link";
import React, { useState } from "react";
import { validateEmail, validatePassword } from "./validator";
import { useAuth } from "@/providers/AuthProvider";
import { useRouter } from "next/navigation";

const LoginPage = () => {
const [value, setValue] = useState({
email: "",
password: "",
});
const [errors, setErrors] = useState({ email: "", password: "" });
const { login } = useAuth();
const router = useRouter();

const handleEmailChange = (e) => {
const val = e.target.value;
setValue((prev) => ({
...prev,
email: val,
}));
setErrors((prev) => ({ ...prev, email: validateEmail(val) }));
};

const handlePasswordChange = (e) => {
const val = e.target.value;
setValue((prev) => ({
...prev,
password: val,
}));
setErrors((prev) => ({ ...prev, password: validatePassword(val) }));
};

// 버튼 활성화 조건
const isFormValid =
value.email !== "" &&
value.password !== "" &&
!errors.email &&
!errors.password;

const handleSubmit = async (e) => {
e.preventDefault();

if (!isFormValid) {
console.log("invalidate");
return;
}

try {
await login(value);

alert("로그인에 성공했습니다.");
router.push("/items");
} catch (error) {
alert(error.message || "로그인에 실패했습니다.");
}
};

return (
<>
<Link href="/">
<AuthLogo />
</Link>
<form onSubmit={handleSubmit} className="w-full flex flex-col gap-6">
<TextInput
name="email"
title="이메일"
placeholder="이메일을 입력해주세요"
value={value.email}
onChange={handleEmailChange}
errmessage={errors.email}
isValid={!errors.email && value.email !== ""}
/>
<PasswordInput
name="password"
title="비밀번호"
placeholder="비밀번호를 입력해주세요"
value={value.password}
onChange={handlePasswordChange}
errmessage={errors.password}
isValid={!errors.password && value.password !== ""}
/>
<button
type="submit"
// disabled={isFormValid}
className={`w-full h-14 flex items-center justify-center rounded-4xl text-gray-100 text-xl font-semibold
${isFormValid ? "bg-Primary-100 cursor-pointer" : "bg-gray-400"}`}
>
로그인
</button>
<OAuth />
<div className="w-full text-center text-sm">
<span className="text-gray-800 mr-1">판다마켓은 처음이신가요?</span>
<Link href="/signup">
<span className="text-Primary-100 underline">회원가입</span>
</Link>
</div>
</form>
</>
);
};

export default LoginPage;
Loading