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
1,258 changes: 1,257 additions & 1 deletion vite-project/package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions vite-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"axios": "^1.11.0",
"dayjs": "^1.11.18",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-responsive": "^10.0.1",
Expand All @@ -22,10 +23,13 @@
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.21",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"vite": "^6.3.5"
}
}
6 changes: 6 additions & 0 deletions vite-project/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
2 changes: 2 additions & 0 deletions vite-project/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Nav from "./components/Nav";
import ItemsPage from "./pages/ItemsPage/ItemsPage";
import AddItemPage from "./pages/AddItemPage/AddItemPage";
import ItemDetailPage from "./pages/ItemDetailPage/ItemDetailPage";

function App() {
return (
Expand All @@ -11,6 +12,7 @@ function App() {
<Route path="/" element={<Navigate to="/items" />} />
<Route path="/items" element={<ItemsPage />} />
<Route path="/additem" element={<AddItemPage />} />
<Route path="/items/:productId" element={<ItemDetailPage />} />
</Routes>
</BrowserRouter>
);
Expand Down
31 changes: 31 additions & 0 deletions vite-project/src/api.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,34 @@ export async function getProducts({
throw new Error(`상품을 불러오는데 실패했습니다 : ${error.message}`);
}
}

export async function getProductDetails(productId) {
try {
const response = await instance.get(`/products/${productId}`);
return response.data;
} catch (error) {
throw new Error(`상품을 불러오는데 실패했습니다 : ${error.message}`);
}
}

export async function getProductComments(productId, limit = 5, cursor) {
try {
const params = { limit };
if (cursor) params.cursor = cursor;
const response = await instance.get(`/products/${productId}/comments`, {
params,
});
return response.data;
} catch (error) {
throw new Error(`상품을 불러오는데 실패했습니다 : ${error.message}`);
}
}

export async function postProducts(body) {
try {
const response = await instance.post("/products/", body);
return response.data;
} catch (error) {
throw new Error(`상품을 등록하는데 실패했습니다 : ${error.message}`);
}
}
Comment on lines +18 to +47
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~ API 함수가 정말 깔끔하네요 👍👍

에러 처리, axios의 파라메터도 적절하게 잘 채워넣으셨구요 ! 굿굿 !

17 changes: 17 additions & 0 deletions vite-project/src/assets/Img-inquiry_empty.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions vite-project/src/assets/ic-back.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions vite-project/src/assets/ic-kebab.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions vite-project/src/assets/ic-profile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 10 additions & 7 deletions vite-project/src/axiosInstance.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,26 @@ const instance = axios.create({
},
});

export const ERROR_STATUSCODE_MESSAGES = {
401: "인증이 필요합니다. 로그인 해주세요.",
403: "접근 권한이 없습니다.",
404: "요청하신 리소스를 찾을 수 없습니다.",
500: "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.",
};
Comment on lines +12 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~ 상태 코드에 맞는 안내메시지도 정의해두셨군요 ! 😉

이렇게 해두면 활용성이 좋겠어요 !


instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response) {
const status = error.response.status;

if (status === 401) {
alert("인증이 필요합니다. 로그인 해주세요.");
} else if (status === 500) {
alert("서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
}
alert(
ERROR_STATUSCODE_MESSAGES[status] || "알 수 없는 오류가 발생했습니다."
);
Comment on lines +26 to +28
Copy link
Collaborator

Choose a reason for hiding this comment

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

공통 API 함수에 alert를 표시해두셨군요 !

이렇게 되면 모든 리소스 요청에 alert가 출력되겠어요 !
예외적으로 처리해야 되는 상황에서는 어떻게 할지 고려해볼 수 있겠네요.

예를 들어서, 디자이너가

  • 판다마켓 상품 등록에서 필수 파라메터가 모두 채워지지 않았을 경우 채워지지 않은 input에 하이라이팅을 해달라는 요청이 있다면 ?
  • 방금 사례는 클라이언트 유효성 검사로 가능하다 친다면, 혹은 회원가입 시 이메일 중복에 의한 에러를 alert가 아닌 toast로 보여주고 싶다면?

모든 통신에 대한 UI를 일관되게 처리하게 된다면 위와 같은 상황에 유연하게 대처하기 어려울 수 있을 것 같아요 !
또한, AxiosInstance에 여러 책임을 가지게 되므로 구조상의 잠재적 문제도 고려해볼 수 있을 것 같아요.
AxiosInstance는 HTTP Client의 역할을 수행하면서 사용자의 UI에도 관여하고 있군요 !

그래서 이러한 문제는 어떻게 해결하면 좋을까?

UI와 관련된 것은 리액트 단(컴포넌트)에서 처리해보는 것을 고려해볼 수 있겠네요 😉
통신 과정에서 에러가 발생하면 로깅을 해두고 에러를 다시 throw하여(이미 잘 해두고 계십니다 !) 컴포넌트에서 예외처리를 하는 방법이 고려될 수 있을 것 같아요 😊

} else {
alert("네트워크 오류가 발생했습니다.");
}

return Promise.reject(error);
}
);
Expand Down
11 changes: 6 additions & 5 deletions vite-project/src/components/SelectDropdown.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useEffect, useRef, useState } from "react";
import selectIcon from "../assets/ic-sort.svg";

const options = [
{ label: "최신순", value: "recent" },
{ label: "좋아요순", value: "favorite" },
];
Comment on lines +4 to +7
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~ 컴포넌트 바깥에 선언해두셨군요 !

좋습니다. 이제 컴포넌트 내부에는 컴포넌트 자원을 사용하는 로직들로만 구성되어 있게되었어요 👍👍


export default function SelectDropdown({ onChange, value }) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);

const options = [
{ label: "최신순", value: "recent" },
{ label: "좋아요순", value: "favorite" },
];

const selected = options.find((opt) => opt.value === value);

const handleSelect = (option) => {
Expand All @@ -32,6 +32,7 @@ export default function SelectDropdown({ onChange, value }) {
<div ref={dropdownRef} className="select-dropdown">
<div className="selected-option" onClick={() => setIsOpen(!isOpen)}>
<img src={selectIcon} alt="드롭다운 아이콘" />
<span>{selected ? selected.label : "선택"}</span>
</div>

{isOpen && (
Expand Down
10 changes: 10 additions & 0 deletions vite-project/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
body {
/* Apply Pretendard font */
@apply font-pretendard;
}
}
1 change: 1 addition & 0 deletions vite-project/src/main.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import App from "./App";
import ReactDOM from "react-dom/client";
import "./index.css";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
15 changes: 13 additions & 2 deletions vite-project/src/pages/AddItemPage/AddItemPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import ProductAddInt from "./ProductAddInt.jsx";
import ProductAddPrice from "./ProductAddPrice.jsx";
import ProductAddTag from "./ProductAddTag.jsx";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { postProducts } from "../../api.jsx";

const FormContainer = Styled.form`
display: flex;
Expand All @@ -22,8 +24,11 @@ function AddItemPage() {
description: "",
price: "",
tags: [],
images: null,
});

const navigate = useNavigate();

const handleChange = (field, value) => {
setFormValues((prev) => ({ ...prev, [field]: value }));
};
Expand All @@ -34,11 +39,17 @@ function AddItemPage() {
formValues.price.trim() !== "" &&
formValues.tags.length > 0;

const handleSubmit = (e) => {
const handleSubmit = async (e) => {
e.preventDefault();
if (!isFormValid) return;

console.log("등록 데이터:", formValues); //submit 확인용
try {
const newItem = { ...formValues, price: Number(formValues.price) };
const response = await postProducts(newItem);
navigate("/items");
} catch (error) {
console.error("상품 등록 실패", error.message);
}
};

return (
Expand Down
20 changes: 10 additions & 10 deletions vite-project/src/pages/AddItemPage/ProductAddImg.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,20 @@ export default function ProductAddImg() {
const inputRef = useRef();

const handleFileChange = (e) => {
const file = e.target.files[0];
const file = e.target.files?.[0];
if (!file) return;

if (previewImg) {
SetShowError(true);
inputRef.current.value = "";
if (!file.type.startsWith("image/")) {
SetShowError("이미지 파일만 업로드할 수 있습니다.");
e.target.value = "";
return;
}

const reader = new FileReader();
reader.onloadend = () => {
setPreviewImg(reader.result);
};
reader.readAsDataURL(file);
SetShowError(null);
inputRef.current = file;
if (previewImg) URL.revokeObjectURL(previewImg);
const url = URL.createObjectURL(file);
setPreviewImg(url);
e.target.value = "";
};

const handleClearClick = () => {
Expand Down
11 changes: 8 additions & 3 deletions vite-project/src/pages/AddItemPage/ProducutAddHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ const ProductAddHeaderButton = styled.button`
font-weight: 600;
font-size: 1.6rem;
color: var(--gray-100);
background-color: ${({ disabled }) =>
disabled ? "var(--gray-400)" : "var(--blue)"};
border-radius: 0.8rem;
width: 7.4rem;
height: 4.2rem;
border: 0.1rem solid var(--gray-400);
cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")};
&:enabled {
background-color: var(--blue);
cursor: pointer;
}
&:disabled {
background-color: var(--gray-400);
cursor: not-allowed;
}
`;

export default function ProductAddHeader({ isFormValid }) {
Expand Down
Loading
Loading