Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[나지원] sprint12 #137

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
25 changes: 25 additions & 0 deletions components/additem/AddItemForm.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.form {
display: flex;
flex-direction: column;
gap: 24px;
}

.header {
display: flex;
justify-content: space-between;
align-items: center;
}

.title {
font-size: 1.25rem;
font-weight: 700;
}

.button {
padding: 12px 23px 12px 23px;
line-height: 1.125rem;
}

.tags {
margin-top: -10px;
}
184 changes: 184 additions & 0 deletions components/additem/AddItemForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {
useState,
useEffect,
ChangeEvent,
KeyboardEvent,
MouseEvent,
} from "react";
import { useRouter } from "next/router";
import { useMutation } from "@tanstack/react-query";
import FileInput from "../ui/FileInput";
import Input from "../ui/Input";
import Textarea from "../ui/Textarea";
import Button from "../ui/Button";
import Tags from "../ui/Tags";
import { uploadImage } from "@/lib/imageService";
import { addProduct } from "@/lib/productService";
import styles from "./AddItemForm.module.css";

interface ProductFormValues {
imgFile: File | null;
product: string;
description: string;
price: string;
tags: string[];
}

type ProductField = keyof ProductFormValues;

export type CreateProductRequestBody = Omit<ProductFormValues, "imgFile"> & {
imgUrl: string;
};

const INITIAL_PRODUCT = {
imgFile: null,
product: "",
description: "",
price: "",
tags: [],
};

const AddItemForm = () => {
const [isDisabled, setIsDisabled] = useState(true);
const [values, setValues] = useState<ProductFormValues>(INITIAL_PRODUCT);
const router = useRouter();

const uploadImageMutation = useMutation({
mutationFn: (imgFile: File) => uploadImage(imgFile),
});

const addProductMutation = useMutation({
mutationFn: (formValues: CreateProductRequestBody) =>
addProduct(formValues),
onSuccess: (id) => {
router.push(`/items/${id}`);
},
});

const handleChange = (
name: string,
value: ProductFormValues[ProductField]
) => {
setValues((prevValues) => {
return {
...prevValues,
[name]: value,
};
});
};

const handleInputChange = (
e: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLTextAreaElement>
) => {
const { name, value } = e.target;
handleChange(name, value);
};

const checkFormEmpty = (values: ProductFormValues) => {
const { imgFile, ...otherValues } = values;

return Object.values(otherValues).some((value) => {
if (Array.isArray(value)) {
return value.length === 0;
}
return value === "";
});
};

const handleSubmit = async (
e: MouseEvent<HTMLButtonElement>
): Promise<void> => {
e.preventDefault();

const { imgFile, ...otherValues } = values;
let imgUrl = "https://example.com/...";

if (imgFile) {
imgUrl = await uploadImageMutation.mutateAsync(imgFile);
}

addProductMutation.mutate({ imgUrl, ...otherValues });
};

const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && e.nativeEvent.isComposing === false) {
e.preventDefault();
e.stopPropagation();

const { name, value } = e.currentTarget;
e.currentTarget.value = "";
if (values.tags.includes(value) || value.trim() === "") return;

handleChange(name, [...values.tags, value]);
}
};

const handleTagRemove = (
e: MouseEvent<HTMLButtonElement>,
target: string
) => {
e.preventDefault();
const nextValue: string[] = values.tags.filter((tag) => tag !== target);
handleChange("tags", nextValue);
};

useEffect(() => {
setIsDisabled(checkFormEmpty(values));
}, [values]);

return (
<form className={styles.form}>
<header className={styles.header}>
<h2 className={styles.title}>상품 등록하기</h2>
<Button
type="submit"
className={styles.button}
disabled={isDisabled}
onClick={handleSubmit}
>
등록
</Button>
</header>
<FileInput
label="상품 이미지"
name="imgFile"
value={values.imgFile}
onChange={handleChange}
/>
<Input
type="text"
label="상품명"
name="product"
placeholder="상품명을 입력해주세요"
value={values.product}
onChange={handleInputChange}
/>
<Textarea
label="상품 소개"
name="description"
placeholder="상품 소개를 입력해주세요"
value={values.description}
onChange={handleInputChange}
/>
<Input
type="text"
label="판매가격"
name="price"
placeholder="판매가격을 입력해주세요"
value={values.price}
onChange={handleInputChange}
/>
<Input
type="text"
label="태그"
name="tags"
placeholder="태그를 입력해주세요"
onKeyDown={handleKeyDown}
className={styles.tags}
/>
<Tags tags={values.tags} onRemove={handleTagRemove} />
</form>
);
};

export default AddItemForm;
10 changes: 7 additions & 3 deletions components/board/Comments.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { useState } from "react";
import Image from "next/image";
import EditCommentForm from "./EditCommentForm";
import Comment from "./Comment";
import { CommentProps } from "@/types/articleTypes";
import styles from "./Comments.module.css";
import replyEmptyImg from "@/public/Img_reply_empty.svg";
import Image from "next/image";
import { CommentProps } from "@/types/articleTypes";

interface CommentsProps {
comments: CommentProps[];
onUpdate: (id: number | null, value: string) => void;
onDelete: (id: number | null) => void;
}

const Comments = ({ comments, onUpdate }: CommentsProps) => {
const Comments = ({ comments, onUpdate, onDelete }: CommentsProps) => {
const [editingId, setEditingId] = useState<number | null>(null);

const handleSelect = (id: number, option: string) => {
if (option === "edit") {
setEditingId(id);
}
if (option === "remove") {
onDelete(id);
}
};

const handleCancel = () => {
Expand Down
153 changes: 153 additions & 0 deletions components/item/ItemDetail.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
.productDetail {
border-bottom: 1px solid var(--gray200);
margin-bottom: 24px;
}

.imageWrapper {
position: relative;
aspect-ratio: 1;
width: 100%;
margin-bottom: 16px;
}

.productImg {
object-fit: cover;
border-radius: 12px;
}

.nameContainer {
display: flex;
justify-content: space-between;
align-items: center;
}

.name {
font-size: 1rem;
font-weight: 600;
color: var(--gray800);
}

.price {
font-size: 1.5rem;
font-weight: 600;
color: var(--gray800);
border-bottom: 1px solid var(--gray200);
padding: 8px 0 16px;
margin-bottom: 16px;
}

.subContainer {
margin-bottom: 24px;
}

.subTitle {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: var(--gray800);
margin-bottom: 8px;
}

.description {
font-size: 1rem;
font-weight: 400;
color: var(--gray800);
}

.userInfo {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0 24px;
}

.authorInfo {
gap: 16px;
}

.authorInfo > div {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
}

.authorInfo img {
width: 40px;
height: 40px;
border-radius: 50%;
}

.authorInfo span {
font-size: 0.875rem;
font-weight: 500;
line-height: 1.5rem;
}

.authorInfo time {
font-size: 0.875rem;
font-weight: 400;
line-height: 1.5rem;
}

.heartButtonContainer {
border-left: 1px solid var(--gray200);
padding-left: 24px;
}

@media screen and (min-width: 768px) {
.productDetail {
display: flex;
gap: 16px;
margin-bottom: 40px;
}

.imageWrapper {
width: 40%;
}

.productContainer {
flex: 1 1 0%;
display: flex;
flex-direction: column;
justify-content: space-between;
}

.name {
font-size: 1.25rem;
}

.price {
font-size: 2rem;
}

.userInfo {
padding-bottom: 32px;
}
}

@media screen and (min-width: 1200px) {
.productDetail {
gap: 24px;
}

.name {
font-size: 1.5rem;
}

.price {
font-size: 2.5rem;
font-weight: 600;
padding: 16px 0;
margin-bottom: 24px;
}

.subTitle {
font-size: 1rem;
margin-bottom: 16px;
}

.userInfo {
padding-bottom: 40px;
}
}
Loading
Loading