Skip to content

Commit dbf4e2a

Browse files
authored
Merge pull request #155 from huiseong29/React-김희성
[김희성] Sprint 6
2 parents aa35265 + 8179084 commit dbf4e2a

File tree

18 files changed

+531
-6
lines changed

18 files changed

+531
-6
lines changed

src/App.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
margin: 0;
33
padding: 0;
44
box-sizing: border-box;
5+
font-family: "Pretendard", sans-serif;
56
}
67

78
#root {

src/api/commentService.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const getComments = async (productId, limit = 5) => {
2+
const response = await fetch(
3+
`https://panda-market-api.vercel.app/products/${productId}/comments?limit=${limit}`
4+
);
5+
6+
if (!response.ok) {
7+
throw new Error("상품 목록 조회에 실패하였습니다.");
8+
}
9+
10+
return await response.json();
11+
};
12+
13+
export const commentServices = {
14+
getComments,
15+
};

src/api/productServices.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ const getProducts = async (
1717
return await response.json();
1818
};
1919

20+
const getProductDetail = async (productId) => {
21+
const response = await fetch(
22+
`https://panda-market-api.vercel.app/products/${productId}`
23+
);
24+
25+
if (!response.ok) {
26+
throw new Error("상품 목록 조회에 실패하였습니다.");
27+
}
28+
29+
return await response.json();
30+
};
31+
2032
export const productServices = {
2133
getProducts,
34+
getProductDetail,
2235
};

src/asset/icon/plus.svg

Lines changed: 4 additions & 0 deletions
Loading

src/asset/icon/x.svg

Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useRef, useState } from "react";
2+
import "./imageUploader.css";
3+
import plus from "../../../asset/icon/plus.svg";
4+
import x from "../../../asset/icon/x.svg";
5+
6+
export default function ImageUploader({ image, setImage }) {
7+
const inputRef = useRef(null);
8+
const [showWarning, setShowWarning] = useState(false);
9+
10+
const handleImageClick = (e) => {
11+
if (image) {
12+
e.preventDefault(); // 이미지 등록 막기
13+
setShowWarning(true);
14+
}
15+
};
16+
17+
const handleImageChange = (e) => {
18+
const file = e.target.files?.[0];
19+
if (!file) return;
20+
21+
const reader = new FileReader();
22+
reader.onloadend = () => {
23+
setImage(reader.result);
24+
};
25+
reader.readAsDataURL(file);
26+
};
27+
28+
const handleDelete = () => {
29+
setImage(null);
30+
setShowWarning(false);
31+
inputRef.current.value = "";
32+
};
33+
34+
return (
35+
<div className="image-wrapper">
36+
<span className="image-title">상품 이미지</span>
37+
38+
<div className="image-boxes">
39+
<label className="upload-label" onClick={handleImageClick}>
40+
<img src={plus} alt="추가 아이콘" />
41+
<p>이미지 등록</p>
42+
<input
43+
type="file"
44+
accept="image/*"
45+
onChange={handleImageChange}
46+
ref={inputRef}
47+
hidden
48+
/>
49+
</label>
50+
51+
{image && (
52+
<div className="preview-box">
53+
<img src={image} alt="미리보기" />
54+
<button className="delete-button" onClick={handleDelete}>
55+
<img src={x} alt="삭제 아이콘" />
56+
</button>
57+
</div>
58+
)}
59+
</div>
60+
61+
{showWarning && (
62+
<p className="warning-message">
63+
*이미지 등록은 최대 1개까지 가능합니다.
64+
</p>
65+
)}
66+
</div>
67+
);
68+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* 전체 wrapper */
2+
.image-wrapper {
3+
display: flex;
4+
flex-direction: column;
5+
margin-bottom: 32px;
6+
}
7+
8+
.image-title {
9+
font-weight: 700;
10+
font-size: 18px;
11+
color: #1f2937;
12+
}
13+
14+
.button {
15+
display: block;
16+
font-weight: 600;
17+
margin-bottom: 8px;
18+
font-size: 18px;
19+
font-weight: 700;
20+
border: none;
21+
}
22+
23+
.image-boxes {
24+
display: flex;
25+
flex-direction: row;
26+
gap: 24px;
27+
margin-top: 16px;
28+
}
29+
30+
.upload-label {
31+
width: 282px;
32+
height: 282px;
33+
border-radius: 12px;
34+
background-color: #f3f4f6;
35+
display: flex;
36+
flex-direction: column;
37+
justify-content: center;
38+
align-items: center;
39+
cursor: pointer;
40+
border: none;
41+
}
42+
43+
.upload-label img {
44+
width: 24px;
45+
height: 24px;
46+
margin-bottom: 12px;
47+
}
48+
49+
.upload-label p {
50+
font-weight: 400;
51+
font-size: 16px;
52+
color: #9ca3af;
53+
}
54+
55+
.preview-box {
56+
position: relative;
57+
width: 282px;
58+
height: 282px;
59+
border-radius: 12px;
60+
}
61+
62+
.preview-box img {
63+
width: 100%;
64+
height: 100%;
65+
object-fit: cover;
66+
border-radius: 12px;
67+
}
68+
69+
.delete-button {
70+
position: absolute;
71+
top: 12px;
72+
right: 12px;
73+
background: none;
74+
border: none;
75+
cursor: pointer;
76+
}
77+
78+
.warning-message {
79+
font-size: 16px;
80+
font-weight: 400;
81+
color: #ef4444;
82+
margin-top: 16px;
83+
}
84+
85+
@media (max-width: 768px) {
86+
}
87+
88+
@media (max-width: 1023px) {
89+
.upload-label {
90+
width: 168px;
91+
height: 168px;
92+
}
93+
94+
.preview-box {
95+
width: 168px;
96+
height: 168px;
97+
}
98+
99+
.image-boxes {
100+
gap: 10px;
101+
}
102+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import "./inputBox.css";
2+
3+
export default function InputBox({
4+
title,
5+
placeholder,
6+
value,
7+
name,
8+
onChange,
9+
onKeyDown,
10+
isInput = true,
11+
height,
12+
}) {
13+
return (
14+
<div className="input-wrapper">
15+
<label>{title}</label>
16+
{isInput ? (
17+
<input
18+
type="text"
19+
name={name ? name : null}
20+
placeholder={placeholder}
21+
value={value}
22+
onChange={onChange}
23+
onKeyDown={onKeyDown ? onKeyDown : null}
24+
style={height ? { height } : undefined}
25+
/>
26+
) : (
27+
<textarea
28+
name={name ? name : null}
29+
placeholder={placeholder}
30+
value={value}
31+
onChange={onChange}
32+
/>
33+
)}
34+
</div>
35+
);
36+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.input-wrapper {
2+
margin-bottom: 32px;
3+
max-width: 1200px;
4+
}
5+
6+
.input-wrapper label {
7+
display: block;
8+
font-weight: 600;
9+
margin-bottom: 16px;
10+
font-size: 18px;
11+
font-weight: 700;
12+
}
13+
14+
.input-wrapper input,
15+
.input-wrapper textarea {
16+
width: 100%;
17+
18+
background-color: #f5f6f8;
19+
border: none;
20+
border-radius: 12px;
21+
padding: 16px 24px;
22+
font-size: 16px;
23+
font-weight: 400;
24+
color: #1f2937;
25+
box-sizing: border-box;
26+
resize: none;
27+
}
28+
29+
.input-wrapper textarea {
30+
height: 282px;
31+
}
32+
33+
.input-wrapper input:focus,
34+
.input-wrapper textarea:focus {
35+
outline: none;
36+
border: 2px solid #3692ff;
37+
}
38+
39+
.input-wrapper input::placeholder,
40+
.input-wrapper textarea::placeholder {
41+
color: #9ca3af;
42+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import InputBox from "../../common/inputbox/InputBox";
2+
import { useState } from "react";
3+
import x from "../../../asset/icon/x.svg";
4+
5+
import "./tagInput.css";
6+
7+
export default function TagInput({ tags, setTags }) {
8+
const [input, setInput] = useState("");
9+
10+
const handleKeyDown = (e) => {
11+
if (e.key === "Enter" && input.trim() !== "") {
12+
e.preventDefault();
13+
14+
const newTag = input.trim();
15+
16+
if (!tags.includes(newTag)) {
17+
setTags([...tags, newTag]);
18+
}
19+
setInput("");
20+
}
21+
};
22+
23+
const handleDelete = (tagToDelete) => {
24+
setTags(tags.filter((tag) => tag !== tagToDelete));
25+
};
26+
27+
return (
28+
<div className="tag-wrapper">
29+
<InputBox
30+
title="태그"
31+
placeholder="태그를 입력해주세요"
32+
value={input}
33+
onChange={(e) => setInput(e.target.value)}
34+
onKeyDown={handleKeyDown}
35+
/>
36+
<div className="tag-list">
37+
{tags.map((tag) => (
38+
<div key={tag} className="tag-item">
39+
<span>#{tag}</span>
40+
<img src={x} alt="태그 삭제" onClick={() => handleDelete(tag)} />
41+
</div>
42+
))}
43+
</div>
44+
</div>
45+
);
46+
}

0 commit comments

Comments
 (0)