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
46 changes: 45 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"dayjs": "^1.11.13",
"next": "15.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.1.4"
"react-hook-form": "^7.54.2",
"zod": "^3.24.1"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.4",
"@eslint/eslintrc": "^3"
"typescript": "^5"
}
}
13 changes: 11 additions & 2 deletions src/components/ArticleItemCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ import heartIcon from "@/assets/icons/heart.svg";
import Image from "next/image";
import { formatDate } from "@/utils/formattedDate";
import Link from "next/link";
import { useMemo } from "react";

export function ArticleItemCard({ id, title, content, image, likeCount, writer, updatedAt }: Article) {
const formattedDate = formatDate(updatedAt);
interface ArticleItemCardProps {
article: Article;
}

export function ArticleItemCard({ article }: ArticleItemCardProps) {
const { id, title, content, image, likeCount, writer, updatedAt } = article;

const formattedDate = useMemo<string>(() => {
return formatDate(updatedAt);
}, [updatedAt]);

return (
<div className={style.container}>
Expand Down
17 changes: 8 additions & 9 deletions src/components/ArticleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,12 @@ interface ArticleListProps {

export default function ArticleList({ initialArticles, initialOrder }: ArticleListProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedLabel, setSelectedLabel] = useState(initialOrder === "like" ? "인기순" : "최신순");
const [orderBy, setOrderBy] = useState<"recent" | "like">(initialOrder);

const handleToggle = () => {
setIsOpen((prev) => !prev);
};
const selectedLabel = orderBy === "like" ? "인기순" : "최신순";

const handleSelect = (label: string, value: string) => {
setSelectedLabel(label);
const newOrder = value === "like" ? "like" : "recent";
setOrderBy(newOrder);
const handleSelect = (label: "최신순" | "인기순", value: "recent" | "like") => {
setOrderBy(value);

// 정렬이 바뀌면 page/게시글 초기화
setPage(1);
Expand All @@ -34,6 +29,10 @@ export default function ArticleList({ initialArticles, initialOrder }: ArticleLi
setIsOpen(false);
};

const handleToggle = () => {
setIsOpen((prev) => !prev);
};

// 검색어 (keyword)
const [keyword, setKeyword] = useState("");

Expand Down Expand Up @@ -132,7 +131,7 @@ export default function ArticleList({ initialArticles, initialOrder }: ArticleLi

<div className={style.all_section}>
{articles.map((article) => (
<ArticleItemCard key={article.id} {...article} />
<ArticleItemCard key={article.id} article={article} />
))}
</div>

Expand Down
13 changes: 11 additions & 2 deletions src/components/BestItemCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ import badge from "@/assets/images/img_badge.svg";
import heart from "@/assets/icons/heart.svg";
import { formatDate } from "@/utils/formattedDate";
import Link from "next/link";
import { useMemo } from "react";

export default function BestItemCard({ id, content, updatedAt, likeCount, writer, image, title }: Article) {
const formattedDate = formatDate(updatedAt);
interface BestItemCardProps {
article: Article;
}

export default function BestItemCard({ article }: BestItemCardProps) {
const { id, title, content, image, updatedAt, writer, likeCount } = article;

const formattedDate = useMemo<string>(() => {
return formatDate(updatedAt);
}, [updatedAt]);

return (
<Link href={`boards/${id}`} className={style.link}>
Expand Down
5 changes: 4 additions & 1 deletion src/components/CommentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import ProfileImg from "@/assets/images/profile.svg";
import { Comment } from "@/types";
import { formatRelativeTime } from "@/utils/formatRelativeTime";
import Image from "next/image";
import { useMemo } from "react";

interface CommentProps {
comment: Comment;
}

export default function CommentCard({ comment }: CommentProps) {
const formatDate = formatRelativeTime(comment.updatedAt);
const formatDate = useMemo<string>(() => {
return formatRelativeTime(comment.updatedAt);
}, [comment.updatedAt]);

return (
<div className={style.container}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface DropdownProps {
isOpen: boolean;
selectedLabel: string;
onToggle: () => void;
onSelect: (label: string, value: string) => void;
onSelect: (label: "최신순" | "인기순", value: "recent" | "like") => void;
}

export default function Dropdown({ isOpen, selectedLabel, onToggle, onSelect }: DropdownProps) {
Expand Down
10 changes: 10 additions & 0 deletions src/components/LoginForm.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,14 @@
color: #3692ff;
text-decoration: underline;
cursor: pointer;
}

.errorInput {
border-color: #f74747;
}

.errorMessage {
color: #f74747;
font-size: 14px;
margin-top: 0.20rem;
}
40 changes: 27 additions & 13 deletions src/components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,51 @@ import styles from "./LoginForm.module.css";
import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { loginSchema, LoginRequest } from "@/types";
import { useForm, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

interface LoginFormProps {
onSubmit: (formData: { email: string; password: string }) => Promise<void>;
onSubmit: (request: LoginRequest) => Promise<void>;
}

export default function LoginForm({ onSubmit }: LoginFormProps) {
const [showPassword, setShowPassword] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<LoginRequest>({
resolver: zodResolver(loginSchema),
});

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await onSubmit({ email, password });
const onFormSubmit: SubmitHandler<LoginRequest> = async (data) => {
try {
await onSubmit(data);
reset();
} catch (err) {
console.error("서버오류", err);
alert("로그인 중 오류가 발생했습니다. 다시 시도해주세요.");
}
};
Comment on lines +15 to 32
Copy link
Collaborator

Choose a reason for hiding this comment

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

네 맞아요ㅎㅎ 이렇게 사용하는거에요.

엄청 잘 하셨는데여! 👍


return (
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<div className={styles.formGroup}>
<label className={styles.label} htmlFor="email">
이메일
</label>
<div className={styles.inputContainer}>
<input
{...register("email")}
type="email"
id="email"
placeholder="이메일을 입력해주세요"
className={styles.inputField}
value={email}
onChange={(e) => setEmail(e.target.value)}
className={`${styles.inputField} ${errors.email ? styles.errorInput : ""}`}
/>
</div>
{errors.email && <p className={styles.errorMessage}>{errors.email.message}</p>}
</div>

<div className={styles.formGroup}>
Expand All @@ -41,17 +55,17 @@ export default function LoginForm({ onSubmit }: LoginFormProps) {
</label>
<div className={styles.inputContainer}>
<input
{...register("password")}
type={showPassword ? "text" : "password"}
id="password"
placeholder="비밀번호를 입력해주세요"
className={styles.inputField}
value={password}
onChange={(e) => setPassword(e.target.value)}
className={`${styles.inputField} ${errors.password ? styles.errorInput : ""}`}
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} className={styles.toggleButton}>
{showPassword ? "O" : "X"}
</button>
</div>
{errors.password && <p className={styles.errorMessage}>{errors.password.message}</p>}
</div>

<button type="submit" className={styles.button}>
Expand Down
10 changes: 10 additions & 0 deletions src/components/SignUpForm.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,14 @@
color: #3692ff;
text-decoration: underline;
cursor: pointer;
}

.errorInput {
border-color: #f74747;
}

.errorMessage {
color: #f74747;
font-size: 14px;
margin-top: 0.20rem;
}
Loading
Loading