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

[최영선] sprint10 #290

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
15 changes: 15 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}
151 changes: 151 additions & 0 deletions components/BestPostList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import Image from "next/image";
import DateTrimmer from "@/utils/TimeTrimmer";
import styled from "styled-components";
import { useState, useEffect } from "react";
import axios from "@/lib/axios";

const Container = styled.div`
display: flex;
gap: 24px;
`;

const StyledTitle = styled.h1`
font-size: 20px;
font-weight: 700;
color: var(--gray-900);
margin-bottom: 24px;
`;

const StyledPostArea = styled.div`
width: 384px;
padding: 0px 24px;
border-radius: 8px;
background-color: var(--gray-50);
`;

const StyledPostTitle = styled.p`
font-size: 20px;
font-weight: 600;
color: var(--gray-800);
`;

const StyledTopArea = styled.div`
display: flex;
justify-content: space-between;
`;

const StyledImageWrapper = styled.img`
width: 72px;
height: 72px;
border: 1px solid var(--gray-200);
border-radius: 6px;
position: relative;
`;

const StyledBottomArea = styled.div`
display: flex;
justify-content: space-between;
`;

const StyledBottomLeftArea = styled.div`
display: flex;
justify-content: space-between;
gap: 8px;
align-items: center;
`;

const StyledNickname = styled.p`
font-size: 14px;
font-weight: 400;
color: var(--gray-600);
`;

const StyledLikeCount = styled.p`
font-size: 14px;
font-weight: 400;
color: var(--gray-500);
`;

const StyledDate = styled.p`
font-size: 14px;
font-weight: 400;
color: var(--gray-400);
`;

type Writer = {
nickname: string;
id: number;
};

type Article = {
updatedAt: string;
createdAt: string;
likeCount: number;
writer: Writer;
image: string;
title: string;
id: number;
};

function BestPostList() {
const [articles, setArticles] = useState<Article[]>([]);

async function getProducts() {
const query = {
orderBy: "like",
page: 1,
pageSize: 3,
};
const res = await axios.get(
`/articles?orderBy=${query.orderBy}&page=${query.page}&pageSize=${query.pageSize}`
);
const nextArticles = res.data.list;
setArticles(nextArticles);
}
Comment on lines +93 to +104
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분은 아래 코드에서도 추가적으로 2번 재사용이 되더라구요~ 아래처럼 모듈로 빼면 더 좋겠어요!

async function fetchArticles(query: ArticleQuery): Promise<Article[]> {
  const queryString = new URLSearchParams({
    orderBy: query.orderBy,
    page: query.page.toString(),
    pageSize: query.pageSize.toString(),
    ...(query.keyword && { keyword: query.keyword }),
  }).toString();

  const res = await axios.get<ArticleResponse>(`/articles?${queryString}`);
  return res.data.list;
}


useEffect(() => {
getProducts();
}, []);
Comment on lines +106 to +108
Copy link
Collaborator

Choose a reason for hiding this comment

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

그렇다면 위에 있는 공통 모듈 가져와서 아래처럼 변경할 수 있을거예요~
에러처리는 모듈에서는 최소한만 진행하고, 각 사용하는 곳에서 구체적인 에러처리 하는 컨셉이예요.

Suggested change
useEffect(() => {
getProducts();
}, []);
useEffect(() => {
async function getBestPosts() {
try {
const bestArticles = await fetchArticles({
orderBy: "like",
page: 1,
pageSize: 3,
});
setArticles(bestArticles);
} catch (error) {
// 에러 처리
}
}
getBestPosts();
}, []);


return (
<>
<StyledTitle>베스트 게시글</StyledTitle>
<Container>
{articles.map((article) => (
<StyledPostArea key={article.id}>
<Image
unoptimized={true}
width={102}
height={30}
src="/image/best_badge.png"
alt="베스트 게시글 뱃지"
/>
<StyledTopArea>
<StyledPostTitle>{article.title}</StyledPostTitle>
<StyledImageWrapper
src={article.image}
alt="게시글 첨부 이미지"
/>
</StyledTopArea>
<StyledBottomArea>
<StyledBottomLeftArea>
<StyledNickname>{article.writer.nickname}</StyledNickname>
<Image
unoptimized={true}
width={15}
height={13}
src="/image/heart_inactive.png"
alt="좋아요 아이콘"
/>
<StyledLikeCount>{article.likeCount}</StyledLikeCount>
</StyledBottomLeftArea>
<StyledDate>{DateTrimmer(article.createdAt)}</StyledDate>
</StyledBottomArea>
</StyledPostArea>
))}
</Container>
</>
);
}

export default BestPostList;
18 changes: 18 additions & 0 deletions components/Button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import styled from "styled-components";

const StyledCommonButton = styled.button`
height: 42px;
padding: 12px 20px;
border-radius: 8px;
margin: auto 0;
border: none;
color: var(--gray-50);
background-color: var(--blue-100);
cursor: pointer;
text-align: center;
align-items: center;
font-size: 18px;
font-weight: 600;
`;

export default StyledCommonButton;
32 changes: 32 additions & 0 deletions components/DropDown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import styled from "styled-components";

const StyledBox = styled.select`
width: 130px;
height: 42px;
padding: 12px 20px;
gap: 10px;
border-radius: 12px;
border: 1px solid var(--gray-200);
`;

interface DropDownProps {
onOrderChange: (orderBy: string) => void;
}

function DropDown({ onOrderChange }: DropDownProps) {
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
onOrderChange(event.target.value);
};

return (
<>
<label htmlFor="order"></label>
<StyledBox onChange={handleChange}>
<option value="recent">최신순</option>
<option value="like">좋아요순</option>
</StyledBox>
</>
);
}

export default DropDown;
84 changes: 84 additions & 0 deletions components/FileInput.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import styled from "styled-components";

const StyledLabel = styled.p`
font-size: 18px;
font-weight: 700;
`;
const StyledPreviewImg = styled.img`
width: 282px;
height: 282px;
border-radius: 12px;
&:hover {
outline: 2px solid var(--blue-100);
}
`;
const StyledFileInputBox = styled.div`
width: 282px;
height: 282px;
border-radius: 12px;
background-color: var(--gray-100);
position: relative;
background-image: url("/image/ic_plus.png");
background-repeat: no-repeat;
background-position-x: 50%;
background-position-y: 40%;
&:hover {
outline: 2px solid var(--blue-100);
}
`;
const StyledFileInputPlaceholder = styled.div`
width: 100%;
text-align: center;
position: absolute;
top: 55%;
color: var(--gray-400);
font-size: 16px;
font-weight: 400;
`;
const StyledFileInput = styled.input`
width: 282px;
height: 282px;
border-radius: 12px;
background-color: var(--gray-100);
opacity: 0;
position: absolute;
cursor: pointer;
`;
const StyledCancelButton = styled.button`
width: 25px;
height: 25px;
position: absolute;
top: 15px;
right: 15px;
background-color: var(--gray-400);
color: var(--gray-50);
border: none;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background-color: var(--blue-100);
}
`;
const StyledFileArea = styled.div`
display: flex;
justify-content: flex-start;
gap: 20px;
`;
const StyledImgArea = styled.div`
position: relative;
display: inline-block;
`;

export {
StyledLabel,
StyledPreviewImg,
StyledFileInputBox,
StyledFileInputPlaceholder,
StyledFileInput,
StyledCancelButton,
StyledFileArea,
StyledImgArea,
};
72 changes: 72 additions & 0 deletions components/FileInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ChangeEvent, useEffect, useRef, useState } from "react";
import {
StyledLabel,
StyledPreviewImg,
StyledFileInputBox,
StyledFileInputPlaceholder,
StyledFileInput,
StyledCancelButton,
StyledFileArea,
StyledImgArea,
} from "./FileInput.styled";

Comment on lines +1 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

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

지난번 리뷰 사항 반영 굳! 👍

interface Props {
name: string;
value: string | File | null;
onChange: (name: string, value: string | File | null) => void;
}

function FileInput({ name, value, onChange }: Props) {
const [preview, setPreview] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const nextValue = e.target.files?.[0] || null;
onChange(name, nextValue);
};

const handleClearClick = () => {
const inputNode = inputRef.current;
if (!inputNode) return;
inputNode.value = "";
onChange(name, null);
};
useEffect(() => {
if (!value) return;

const nextPreview: string = URL.createObjectURL(value as File);
setPreview(nextPreview);

return () => {
setPreview(null);
URL.revokeObjectURL(nextPreview);
};
}, [value]);

return (
<div>
<StyledLabel>이미지</StyledLabel>
<StyledFileArea>
<StyledFileInputBox>
<StyledFileInput
type="file"
accept="image/png, image/jpeg"
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

onChange={handleChange}
ref={inputRef}
/>
<StyledFileInputPlaceholder>이미지 등록</StyledFileInputPlaceholder>
</StyledFileInputBox>
<StyledImgArea>
{value ? (
<StyledCancelButton onClick={handleClearClick}>
X
</StyledCancelButton>
) : null}
{preview && <StyledPreviewImg src={preview} alt="이미지 미리보기" />}
</StyledImgArea>
</StyledFileArea>
</div>
);
}

export default FileInput;
Loading
Loading