Skip to content

Commit 811d6b8

Browse files
authored
Merge pull request #125 from pagers-org/carpe/article-detail
feat(article): 아티클 상세 페이지를 코멘트 영역을 제외하고 실제 데이터와 연동합니다.
2 parents 36b3d97 + 10c5aa2 commit 811d6b8

23 files changed

+450
-270
lines changed

apps/react-world/src/apis/apiInstances.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import axios, { AxiosRequestConfig } from 'axios';
1+
import type { AxiosRequestConfig } from 'axios';
2+
import axios from 'axios';
23

34
// TODO: 향후 .env 파일로 분리
45
const BASE_URL = 'https://api.realworld.io/api';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface ArticleAuthor {
2+
username: string;
3+
bio: string;
4+
image: string;
5+
following: boolean;
6+
}

apps/react-world/src/apis/article/ArticlePreviewService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { ArticlePreviewResponse } from './ArticlePreviewService.types';
44

55
export const ARTICLE_PREVIEW_FETCH_LIMIT = 10;
66

7-
class ArticleService {
7+
class ArticlePreviewService {
88
static async fetchArticlePreviews(
99
pageIndex: number,
1010
): Promise<ArticlePreviewResponse> {
@@ -28,4 +28,4 @@ class ArticleService {
2828
}
2929
}
3030

31-
export default ArticleService;
31+
export default ArticlePreviewService;

apps/react-world/src/apis/article/ArticlePreviewService.types.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1+
import type { ArticleAuthor } from './Article.types';
2+
13
export interface ArticlePreviewParams {
24
offset: number;
35
limit: number;
46
}
57

68
// TODO: 클라이언트에서 좀 더 가독성 높은 선언이 되도록 프로퍼티 이름 변경하고 매핑 로직 작성할 것
7-
export interface ArticleAuthor {
8-
username: string;
9-
bio: string;
10-
image: string;
11-
following: boolean;
12-
}
13-
149
export interface ArticlePreviewData {
1510
slug: string;
1611
title: string;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { isAxiosError } from 'axios';
2+
import { api } from '../apiInstances';
3+
import type {
4+
ArticleDetailResponse,
5+
ArticleDetailErrorResponse,
6+
} from './ArticleService.types';
7+
8+
class ArticleService {
9+
static async fetchArticleDetail(
10+
slug: string,
11+
): Promise<ArticleDetailResponse> {
12+
try {
13+
const response = await api.get(`/articles/${slug}`);
14+
return response.data;
15+
} catch (error) {
16+
if (isAxiosError(error) && error.response) {
17+
console.error('Axios error occurred:', error.response.data);
18+
throw error.response.data as ArticleDetailErrorResponse;
19+
}
20+
console.error('An unexpected error occurred:', error);
21+
throw error;
22+
}
23+
}
24+
}
25+
26+
export default ArticleService;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { ArticleAuthor } from './Article.types';
2+
3+
export interface ArticleDetailData {
4+
slug: string;
5+
title: string;
6+
description: string;
7+
body: string;
8+
tagList: string[];
9+
createdAt: string;
10+
updatedAt: string;
11+
favorited: boolean;
12+
favoritesCount: number;
13+
author: ArticleAuthor;
14+
}
15+
16+
export interface ArticleDetailResponse {
17+
article: ArticleDetailData;
18+
}
19+
20+
export interface ArticleDetailErrorResponse {
21+
errors: {
22+
body: string[];
23+
};
24+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { ArticleAuthor } from '@apis/article/Article.types';
2+
import { formatDate } from '@utils/dateUtils';
3+
4+
interface ArticleActionsProps {
5+
author: ArticleAuthor;
6+
createdAt: string;
7+
favorited: boolean;
8+
favoritesCount: number;
9+
}
10+
11+
const ArticleActions = (props: ArticleActionsProps) => {
12+
const { author, createdAt, favorited, favoritesCount } = props;
13+
14+
return (
15+
<div className="article-actions">
16+
<div className="article-meta">
17+
<a href={`profile/${author.username}`}>
18+
<img src={author.image || ''} alt={author.username} />
19+
</a>
20+
<div className="info">
21+
<a href={`profile/${author.username}`} className="author">
22+
{author.username}
23+
</a>
24+
<span className="date">{formatDate(createdAt)}</span>
25+
</div>
26+
<button className="btn btn-sm btn-outline-secondary">
27+
<i className="ion-plus-round"></i>
28+
&nbsp; Follow {author.username}
29+
</button>
30+
&nbsp;
31+
<button
32+
className={`btn btn-sm ${
33+
favorited ? 'btn-primary' : 'btn-outline-primary'
34+
}`}
35+
>
36+
<i className="ion-heart"></i>
37+
&nbsp; {favorited ? 'Unfavorite' : 'Favorite'} Article{' '}
38+
<span className="counter">({favoritesCount})</span>
39+
</button>
40+
</div>
41+
</div>
42+
);
43+
};
44+
45+
export default ArticleActions;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
const ArticleComments = () => {
2+
return (
3+
<div className="row">
4+
<div className="col-xs-12 col-md-8 offset-md-2">
5+
<form className="card comment-form">
6+
<div className="card-block">
7+
<textarea
8+
className="form-control"
9+
placeholder="Write a comment..."
10+
></textarea>
11+
</div>
12+
<div className="card-footer">
13+
<img
14+
src="http://i.imgur.com/Qr71crq.jpg"
15+
className="comment-author-img"
16+
/>
17+
<button className="btn btn-sm btn-primary">Post Comment</button>
18+
</div>
19+
</form>
20+
21+
<div className="card">
22+
<div className="card-block">
23+
<p className="card-text">
24+
With supporting text below as a natural lead-in to additional
25+
content.
26+
</p>
27+
</div>
28+
<div className="card-footer">
29+
<a href="/profile/author" className="comment-author">
30+
<img
31+
src="http://i.imgur.com/Qr71crq.jpg"
32+
className="comment-author-img"
33+
/>
34+
</a>
35+
&nbsp;
36+
<a href="/profile/jacob-schmidt" className="comment-author">
37+
Jacob Schmidt
38+
</a>
39+
<span className="date-posted">Dec 29th</span>
40+
</div>
41+
</div>
42+
43+
<div className="card">
44+
<div className="card-block">
45+
<p className="card-text">
46+
With supporting text below as a natural lead-in to additional
47+
content.
48+
</p>
49+
</div>
50+
<div className="card-footer">
51+
<a href="/profile/author" className="comment-author">
52+
<img
53+
src="http://i.imgur.com/Qr71crq.jpg"
54+
className="comment-author-img"
55+
/>
56+
</a>
57+
&nbsp;
58+
<a href="/profile/jacob-schmidt" className="comment-author">
59+
Jacob Schmidt
60+
</a>
61+
<span className="date-posted">Dec 29th</span>
62+
<span className="mod-options">
63+
<i className="ion-trash-a"></i>
64+
</span>
65+
</div>
66+
</div>
67+
</div>
68+
</div>
69+
);
70+
};
71+
72+
export default ArticleComments;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
interface ArticleContentsProps {
2+
body: string;
3+
tagList: string[];
4+
}
5+
6+
const ArticleContents = (props: ArticleContentsProps) => {
7+
const { body, tagList } = props;
8+
9+
return (
10+
<div className="row article-content">
11+
<div className="col-md-12">
12+
<p>{body}</p>
13+
<ul className="tag-list">
14+
{tagList.map((tag, index) => (
15+
<li key={index} className="tag-default tag-pill tag-outline">
16+
{tag}
17+
</li>
18+
))}
19+
</ul>
20+
</div>
21+
</div>
22+
);
23+
};
24+
25+
export default ArticleContents;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { formatDate } from '@utils/dateUtils';
2+
3+
interface ArticleHeaderProps {
4+
title: string;
5+
authorName: string;
6+
authorImage: string;
7+
createdAt: string;
8+
favoritesCount: number;
9+
}
10+
11+
const ArticleHeader = (props: ArticleHeaderProps) => {
12+
const { title, authorName, authorImage, createdAt, favoritesCount } = props;
13+
14+
return (
15+
<div className="banner">
16+
<div className="container">
17+
<h1>{title}</h1>
18+
19+
<div className="article-meta">
20+
<a href={`/profile/${authorName}`}>
21+
<img src={authorImage} alt={authorName} />
22+
</a>
23+
<div className="info">
24+
<a href={`/profile/${authorName}`} className="author">
25+
{authorName}
26+
</a>
27+
<span className="date">{formatDate(createdAt)}</span>
28+
</div>
29+
<button className="btn btn-sm btn-outline-secondary">
30+
<i className="ion-plus-round"></i>
31+
&nbsp; Follow {authorName}{' '}
32+
<span className="counter">({favoritesCount})</span>
33+
</button>
34+
&nbsp;&nbsp;
35+
<button className="btn btn-sm btn-outline-primary">
36+
<i className="ion-heart"></i>
37+
&nbsp; Favorite Post <span className="counter">(29)</span>
38+
</button>
39+
<button className="btn btn-sm btn-outline-secondary">
40+
<i className="ion-edit"></i> Edit Article
41+
</button>
42+
<button className="btn btn-sm btn-outline-danger">
43+
<i className="ion-trash-a"></i> Delete Article
44+
</button>
45+
</div>
46+
</div>
47+
</div>
48+
);
49+
};
50+
51+
export default ArticleHeader;

0 commit comments

Comments
 (0)