Skip to content

Commit 0c6d28e

Browse files
authored
Merge pull request #317 from rak517/Next-최성락-sprint11
[최성락] Sprint11
2 parents fc5dbca + 5208ada commit 0c6d28e

24 files changed

+442
-200
lines changed

package-lock.json

Lines changed: 45 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,21 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@hookform/resolvers": "^3.10.0",
13+
"dayjs": "^1.11.13",
14+
"next": "15.1.4",
1215
"react": "^19.0.0",
1316
"react-dom": "^19.0.0",
14-
"next": "15.1.4"
17+
"react-hook-form": "^7.54.2",
18+
"zod": "^3.24.1"
1519
},
1620
"devDependencies": {
17-
"typescript": "^5",
21+
"@eslint/eslintrc": "^3",
1822
"@types/node": "^20",
1923
"@types/react": "^19",
2024
"@types/react-dom": "^19",
2125
"eslint": "^9",
2226
"eslint-config-next": "15.1.4",
23-
"@eslint/eslintrc": "^3"
27+
"typescript": "^5"
2428
}
2529
}

src/components/ArticleItemCard.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@ import heartIcon from "@/assets/icons/heart.svg";
55
import Image from "next/image";
66
import { formatDate } from "@/utils/formattedDate";
77
import Link from "next/link";
8+
import { useMemo } from "react";
89

9-
export function ArticleItemCard({ id, title, content, image, likeCount, writer, updatedAt }: Article) {
10-
const formattedDate = formatDate(updatedAt);
10+
interface ArticleItemCardProps {
11+
article: Article;
12+
}
13+
14+
export function ArticleItemCard({ article }: ArticleItemCardProps) {
15+
const { id, title, content, image, likeCount, writer, updatedAt } = article;
16+
17+
const formattedDate = useMemo<string>(() => {
18+
return formatDate(updatedAt);
19+
}, [updatedAt]);
1120

1221
return (
1322
<div className={style.container}>

src/components/ArticleList.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,12 @@ interface ArticleListProps {
1414

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

20-
const handleToggle = () => {
21-
setIsOpen((prev) => !prev);
22-
};
19+
const selectedLabel = orderBy === "like" ? "인기순" : "최신순";
2320

24-
const handleSelect = (label: string, value: string) => {
25-
setSelectedLabel(label);
26-
const newOrder = value === "like" ? "like" : "recent";
27-
setOrderBy(newOrder);
21+
const handleSelect = (label: "최신순" | "인기순", value: "recent" | "like") => {
22+
setOrderBy(value);
2823

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

32+
const handleToggle = () => {
33+
setIsOpen((prev) => !prev);
34+
};
35+
3736
// 검색어 (keyword)
3837
const [keyword, setKeyword] = useState("");
3938

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

133132
<div className={style.all_section}>
134133
{articles.map((article) => (
135-
<ArticleItemCard key={article.id} {...article} />
134+
<ArticleItemCard key={article.id} article={article} />
136135
))}
137136
</div>
138137

src/components/BestItemCard.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@ import badge from "@/assets/images/img_badge.svg";
55
import heart from "@/assets/icons/heart.svg";
66
import { formatDate } from "@/utils/formattedDate";
77
import Link from "next/link";
8+
import { useMemo } from "react";
89

9-
export default function BestItemCard({ id, content, updatedAt, likeCount, writer, image, title }: Article) {
10-
const formattedDate = formatDate(updatedAt);
10+
interface BestItemCardProps {
11+
article: Article;
12+
}
13+
14+
export default function BestItemCard({ article }: BestItemCardProps) {
15+
const { id, title, content, image, updatedAt, writer, likeCount } = article;
16+
17+
const formattedDate = useMemo<string>(() => {
18+
return formatDate(updatedAt);
19+
}, [updatedAt]);
1120

1221
return (
1322
<Link href={`boards/${id}`} className={style.link}>

src/components/CommentCard.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import ProfileImg from "@/assets/images/profile.svg";
33
import { Comment } from "@/types";
44
import { formatRelativeTime } from "@/utils/formatRelativeTime";
55
import Image from "next/image";
6+
import { useMemo } from "react";
67

78
interface CommentProps {
89
comment: Comment;
910
}
1011

1112
export default function CommentCard({ comment }: CommentProps) {
12-
const formatDate = formatRelativeTime(comment.updatedAt);
13+
const formatDate = useMemo<string>(() => {
14+
return formatRelativeTime(comment.updatedAt);
15+
}, [comment.updatedAt]);
1316

1417
return (
1518
<div className={style.container}>

src/components/Dropdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ interface DropdownProps {
66
isOpen: boolean;
77
selectedLabel: string;
88
onToggle: () => void;
9-
onSelect: (label: string, value: string) => void;
9+
onSelect: (label: "최신순" | "인기순", value: "recent" | "like") => void;
1010
}
1111

1212
export default function Dropdown({ isOpen, selectedLabel, onToggle, onSelect }: DropdownProps) {

src/components/LoginForm.module.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,14 @@
115115
color: #3692ff;
116116
text-decoration: underline;
117117
cursor: pointer;
118+
}
119+
120+
.errorInput {
121+
border-color: #f74747;
122+
}
123+
124+
.errorMessage {
125+
color: #f74747;
126+
font-size: 14px;
127+
margin-top: 0.20rem;
118128
}

src/components/LoginForm.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,51 @@ import styles from "./LoginForm.module.css";
22
import { useState } from "react";
33
import Image from "next/image";
44
import Link from "next/link";
5+
import { loginSchema, LoginRequest } from "@/types";
6+
import { useForm, SubmitHandler } from "react-hook-form";
7+
import { zodResolver } from "@hookform/resolvers/zod";
58

69
interface LoginFormProps {
7-
onSubmit: (formData: { email: string; password: string }) => Promise<void>;
10+
onSubmit: (request: LoginRequest) => Promise<void>;
811
}
912

1013
export default function LoginForm({ onSubmit }: LoginFormProps) {
1114
const [showPassword, setShowPassword] = useState(false);
12-
const [email, setEmail] = useState("");
13-
const [password, setPassword] = useState("");
15+
const {
16+
register,
17+
handleSubmit,
18+
formState: { errors },
19+
reset,
20+
} = useForm<LoginRequest>({
21+
resolver: zodResolver(loginSchema),
22+
});
1423

15-
const handleSubmit = async (e: React.FormEvent) => {
16-
e.preventDefault();
17-
await onSubmit({ email, password });
24+
const onFormSubmit: SubmitHandler<LoginRequest> = async (data) => {
25+
try {
26+
await onSubmit(data);
27+
reset();
28+
} catch (err) {
29+
console.error("서버오류", err);
30+
alert("로그인 중 오류가 발생했습니다. 다시 시도해주세요.");
31+
}
1832
};
1933

2034
return (
21-
<form onSubmit={handleSubmit}>
35+
<form onSubmit={handleSubmit(onFormSubmit)}>
2236
<div className={styles.formGroup}>
2337
<label className={styles.label} htmlFor="email">
2438
이메일
2539
</label>
2640
<div className={styles.inputContainer}>
2741
<input
42+
{...register("email")}
2843
type="email"
2944
id="email"
3045
placeholder="이메일을 입력해주세요"
31-
className={styles.inputField}
32-
value={email}
33-
onChange={(e) => setEmail(e.target.value)}
46+
className={`${styles.inputField} ${errors.email ? styles.errorInput : ""}`}
3447
/>
3548
</div>
49+
{errors.email && <p className={styles.errorMessage}>{errors.email.message}</p>}
3650
</div>
3751

3852
<div className={styles.formGroup}>
@@ -41,17 +55,17 @@ export default function LoginForm({ onSubmit }: LoginFormProps) {
4155
</label>
4256
<div className={styles.inputContainer}>
4357
<input
58+
{...register("password")}
4459
type={showPassword ? "text" : "password"}
4560
id="password"
4661
placeholder="비밀번호를 입력해주세요"
47-
className={styles.inputField}
48-
value={password}
49-
onChange={(e) => setPassword(e.target.value)}
62+
className={`${styles.inputField} ${errors.password ? styles.errorInput : ""}`}
5063
/>
5164
<button type="button" onClick={() => setShowPassword(!showPassword)} className={styles.toggleButton}>
5265
{showPassword ? "O" : "X"}
5366
</button>
5467
</div>
68+
{errors.password && <p className={styles.errorMessage}>{errors.password.message}</p>}
5569
</div>
5670

5771
<button type="submit" className={styles.button}>

src/components/SignUpForm.module.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,14 @@
115115
color: #3692ff;
116116
text-decoration: underline;
117117
cursor: pointer;
118+
}
119+
120+
.errorInput {
121+
border-color: #f74747;
122+
}
123+
124+
.errorMessage {
125+
color: #f74747;
126+
font-size: 14px;
127+
margin-top: 0.20rem;
118128
}

0 commit comments

Comments
 (0)