Skip to content

Commit 394e1ca

Browse files
committed
feat: nextjs - compliant detail page metadata (#develop)
1 parent 9e4bc44 commit 394e1ca

File tree

8 files changed

+289
-4950
lines changed

8 files changed

+289
-4950
lines changed

services/ahhachul.com/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ dist-ssr
2525
/test-results/
2626
e2e/playwright-report/
2727
/blob-report/
28-
/playwright/.cache/
28+
/playwright/.cache/

services/ahhachul.com/stats.html

Lines changed: 0 additions & 4949 deletions
This file was deleted.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
'use client';
2+
3+
import { useQuery } from '@tanstack/react-query';
4+
5+
import { formatDateTime } from '@ahhachul/utils';
6+
7+
import { ReadonlyEditor } from '@/component/Editor';
8+
import { TIMESTAMP } from '@/constant';
9+
import { cn, extractTextFromLexical, isLexicalContent } from '@/util';
10+
11+
import { ComplaintTypeBadge } from './ComplaintTypeBadge';
12+
13+
import { getComplaintDetailPost } from '../_lib/getDetailPost';
14+
15+
type Props = {
16+
id: number;
17+
};
18+
19+
export default function CommunityPostDetail({ id }: Props) {
20+
const { data: post } = useQuery({
21+
queryKey: ['complaint-post', id],
22+
queryFn: getComplaintDetailPost,
23+
staleTime: 5 * TIMESTAMP.MINUTE,
24+
select: res => res.result,
25+
});
26+
27+
console.log('post.result:', post);
28+
29+
if (!post) return null;
30+
31+
const title = extractTextFromLexical(post.content, post.complaintType).slice(0, 20);
32+
33+
return (
34+
<>
35+
<article>
36+
{/* <BaseArticleImages label={post.title} images={images} /> */}
37+
<div className=" pt-5 px-5">
38+
<ComplaintTypeBadge complaintType={post.complaintType} />
39+
<div className=" text-title-large text-gray-90 line-clamp-2 pt-[13px] pb-4">{title}</div>
40+
<div className=" w-full flex items-center justify-between pb-4 border-b border-b-gray-20">
41+
<div className=" flex items-center gap-1 text-body-medium">
42+
<span className=" text-gray-80">{post.writer || '로스트 112'}</span>
43+
<span className=" text-gray-70">{formatDateTime(post.createdAt!)}</span>
44+
</div>
45+
<div className=" flex items-center text-gray-90 text-label-medium font-regular">
46+
{/* {SUBWAY_LOGO_SVG_LIST[post.subwayLineId]} */}
47+
</div>
48+
</div>
49+
</div>
50+
51+
<div className=" px-5">
52+
{isLexicalContent(post.content) ? (
53+
<ReadonlyEditor
54+
content={post.content}
55+
className={cn('px-0', 'py-6', '[&>div>div]:p-0', '[&>div>div]:border-none')}
56+
/>
57+
) : (
58+
<p className=" py-6 mb-3 text-body-large-semi text-gray-90">{post.content}</p>
59+
)}
60+
</div>
61+
</article>
62+
63+
{/* <LostFoundCommentList commentCnt={post.commentCnt} articleId={lostId} /> */}
64+
{/* <CommentTextField
65+
placeholder={`${post.writer ?? '로스트 112'}에게 댓글을 남겨주세요.`}
66+
onSubmit={handleSubmitComment}
67+
onChange={handleChangeComment}
68+
/> */}
69+
</>
70+
);
71+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use client';
2+
3+
import type { ComplaintType } from '@/types/complaint';
4+
5+
const complaintTypeOptions: Record<ComplaintType, string> = {
6+
ENVIRONMENTAL_COMPLAINT: '환경민원',
7+
TEMPERATURE_CONTROL: '온도조절',
8+
DISORDER: '질서저해',
9+
ANNOUNCEMENT: '안내방송',
10+
EMERGENCY_PATIENT: '응급환자',
11+
VIOLENCE: '폭력',
12+
SEXUAL_HARASSMENT: '성추행',
13+
};
14+
15+
interface Props {
16+
complaintType: ComplaintType;
17+
}
18+
19+
export const ComplaintTypeBadge = ({ complaintType }: Props) => {
20+
return (
21+
<div className=" h-7 text-label-small text-gray-0 px-2.5 flex items-center justify-center bg-[#407AD6] rounded-[100px] w-max">
22+
{complaintTypeOptions[complaintType]}
23+
</div>
24+
);
25+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { QueryFunction } from '@tanstack/react-query';
2+
3+
import type { IResponse } from '@/types';
4+
import { ComplaintPostDetail } from '@/types/complaint';
5+
6+
export const getComplaintDetailPost: QueryFunction<
7+
IResponse<ComplaintPostDetail>,
8+
[_1: string, id: number]
9+
> = async ({ queryKey }) => {
10+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
11+
const [_1, id] = queryKey;
12+
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/complaintcomplaint-posts/${id}`, {
13+
next: {
14+
tags: ['complaint-post', id.toString()],
15+
},
16+
credentials: 'include',
17+
});
18+
19+
if (!res.ok) {
20+
// This will activate the closest `error.js` Error Boundary
21+
throw new Error('Failed to fetch data');
22+
}
23+
24+
return res.json();
25+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { cookies } from 'next/headers';
2+
3+
export const getComplaintDetailPostServer = async ({
4+
queryKey,
5+
}: {
6+
queryKey: [string, number];
7+
}) => {
8+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
9+
const [_1, id] = queryKey;
10+
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/complaint-posts/${id}`, {
11+
next: {
12+
revalidate: 3600,
13+
tags: ['complaint-post', id.toString()],
14+
},
15+
credentials: 'include',
16+
headers: { Cookie: (await cookies()).toString() },
17+
});
18+
19+
if (!res.ok) {
20+
// This will activate the closest `error.js` Error Boundary
21+
throw new Error('Failed to fetch data');
22+
}
23+
24+
return res.json();
25+
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
2+
import type { Metadata } from 'next';
3+
4+
import { SUBWAY_LINES } from '@/constant';
5+
import { extractTextFromLexical } from '@/util';
6+
7+
import ComplaintDetail from './_components/ComplaintDetail';
8+
import { getComplaintDetailPostServer } from './_lib/getDetailPostServer';
9+
10+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
11+
const { id } = await params;
12+
const post = await getComplaintDetailPostServer({ queryKey: ['complaint-post', id] });
13+
14+
const subwayLineId = post.result.subwayLineId;
15+
const extractTitle = extractTextFromLexical(post.result.content, post.result.complaintType).slice(
16+
0,
17+
16,
18+
);
19+
const baseTitle = (subwayLineId?: string) =>
20+
`${extractTitle} / ${subwayLineId} 민원 접수 - 아하철`;
21+
22+
const title =
23+
subwayLineId && +subwayLineId !== 0
24+
? baseTitle(SUBWAY_LINES.find(subway => subway.id === +subwayLineId)?.name)
25+
: baseTitle('지하철');
26+
27+
const baseDescription =
28+
'지하철 이용 중 불편사항을 쉽고 빠르게 신고하세요. 시설물 고장, 불편사항, 개선 요청 등 다양한 민원을 실시간으로 접수하고 처리 현황을 확인할 수 있습니다. 더 나은 지하철 환경을 만드는 첫걸음, 아하철 민원 서비스입니다.';
29+
30+
const image =
31+
post.result.images.length > 0 && post.result.images.at(0).imageUrl
32+
? post.result.images.at(0).imageUrl
33+
: subwayLineId && +subwayLineId !== 0
34+
? `https://static.dev.ahhachul.com/banners/complaint/subway-line-${subwayLineId}.png`
35+
: 'https://static.dev.ahhachul.com/banners/complaint/main.png';
36+
37+
return {
38+
title,
39+
description: extractTextFromLexical(post.result.content, baseDescription),
40+
openGraph: {
41+
title,
42+
description: extractTextFromLexical(post.result.content, baseDescription),
43+
images: [
44+
{
45+
url: image,
46+
width: 800,
47+
height: 400,
48+
},
49+
],
50+
},
51+
};
52+
}
53+
54+
type Props = {
55+
params: Promise<{
56+
id: number;
57+
}>;
58+
};
59+
60+
export default async function ComplaintDetailPage(props: Props) {
61+
const { id } = await props.params;
62+
const queryClient = new QueryClient();
63+
await queryClient.prefetchQuery({
64+
queryKey: ['complaint-post', id],
65+
queryFn: getComplaintDetailPostServer,
66+
});
67+
const dehydratedState = dehydrate(queryClient);
68+
69+
return (
70+
<main className="flex min-h-screen flex-col text-black bg-white mb-[210px]">
71+
<HydrationBoundary state={dehydratedState}>
72+
<ComplaintDetail id={id} />
73+
</HydrationBoundary>
74+
</main>
75+
);
76+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { IPost, CursorPagination, IPostImage, SubwayLineFilterOptions } from './common';
2+
3+
export type ComplaintType =
4+
| 'ENVIRONMENTAL_COMPLAINT'
5+
| 'TEMPERATURE_CONTROL'
6+
| 'DISORDER'
7+
| 'ANNOUNCEMENT'
8+
| 'EMERGENCY_PATIENT'
9+
| 'VIOLENCE'
10+
| 'SEXUAL_HARASSMENT';
11+
12+
export type ShortComplaintType =
13+
| 'WASTE'
14+
| 'VOMIT'
15+
| 'VENTILATION_REQUEST'
16+
| 'NOISY'
17+
| 'NOT_HEARD'
18+
| 'TOO_HOT'
19+
| 'TOO_COLD'
20+
| 'MOBILE_VENDOR'
21+
| 'DRUNK'
22+
| 'HOMELESS'
23+
| 'BEGGING'
24+
| 'RELIGIOUS_ACTIVITY'
25+
| 'SELF'
26+
| 'WITNESS'
27+
| 'VICTIM';
28+
29+
export type ComplaintStatus = 'CREATED' | 'DONE';
30+
31+
export interface ComplaintPost extends IPost {
32+
complaintType: ComplaintType;
33+
shortContentType: ShortComplaintType;
34+
trainNo: string;
35+
phoneNumber: string;
36+
location: number;
37+
status: ComplaintStatus;
38+
}
39+
40+
export interface ComplaintPostDetail extends ComplaintPost {
41+
images: IPostImage[];
42+
}
43+
44+
export interface ComplaintListParams<TSubwayLine = number> extends Partial<CursorPagination> {
45+
subwayLineId: TSubwayLine;
46+
keyword?: string;
47+
}
48+
49+
export interface ComplaintForm {
50+
title: string;
51+
content: string;
52+
subwayLineId: number;
53+
complaintType: ComplaintType;
54+
shortContentType: ShortComplaintType;
55+
images: File[];
56+
}
57+
58+
export type ComplaintFilterKeys = 'subwayLineId';
59+
60+
export type ComplaintFilterValues = {
61+
subwayLineId: SubwayLineFilterOptions;
62+
};
63+
64+
export type ComplaintFilters = {
65+
[K in ComplaintFilterKeys]: ComplaintFilterValues[K];
66+
};

0 commit comments

Comments
 (0)