Skip to content

Commit c9869fc

Browse files
authored
Merge pull request #32 from DeveloperBlog-Devflow/feature/docs-page
feat : 개발 일지 페이지 구현
2 parents 3d58855 + 2909ef3 commit c9869fc

10 files changed

Lines changed: 2584 additions & 182 deletions

File tree

app/(with-sidebar)/layout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import Sidebar from '@/components/common/Sidebar';
22

3+
import '@uiw/react-md-editor/markdown-editor.css';
4+
import '@uiw/react-markdown-preview/markdown.css';
5+
36
export default function SidebarLayout({
47
children,
58
}: {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import PostDetail from '@/components/write/PostDetail';
2+
3+
type Props = {
4+
params: Promise<{ tilId: string }>;
5+
};
6+
7+
const Page = async ({ params }: Props) => {
8+
const { tilId } = await params;
9+
return <PostDetail tilId={tilId} />;
10+
};
11+
12+
export default Page;

app/(with-sidebar)/write/page.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Editor from '@/components/write/Editor';
2+
3+
const Page = () => {
4+
return (
5+
<div className="bg-background flex min-h-screen flex-col gap-8 p-4">
6+
<Editor />
7+
</div>
8+
);
9+
};
10+
11+
export default Page;

components/common/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const navItems = [
1616
{ label: '홈', href: '/', icon: Home },
1717
{ label: '개발 일지', href: '/logs', icon: ClipboardList },
1818
{ label: '새 계획 만들기', href: '/plans/new', icon: CalendarPlus },
19-
{ label: '새 페이지 만들기', href: '/pages/new', icon: CopyPlus },
19+
{ label: '새 페이지 만들기', href: '/write', icon: CopyPlus },
2020
];
2121

2222
const Sidebar = () => {

components/write/Editor.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use client';
2+
3+
import dynamic from 'next/dynamic';
4+
import { useState } from 'react';
5+
import type { MDEditorProps } from '@uiw/react-md-editor';
6+
import SaveButton from '@/components/write/SaveButton';
7+
import { createTil } from '@/services/write/til.service';
8+
import { auth } from '@/lib/firebase';
9+
import { useRouter } from 'next/navigation';
10+
11+
const MDEditor = dynamic<MDEditorProps>(() => import('@uiw/react-md-editor'), {
12+
ssr: false,
13+
});
14+
15+
const Editor = () => {
16+
const [value, setValue] = useState<string>('');
17+
const [title, setTitle] = useState<string>('');
18+
const router = useRouter();
19+
20+
const onClickCancel = () => {
21+
setValue('');
22+
};
23+
24+
const onClickSave = async () => {
25+
const user = auth.currentUser;
26+
if (!user) {
27+
alert('로그인이 필요합니다');
28+
return;
29+
}
30+
if (!title.trim()) {
31+
alert('제목을 입력하세요');
32+
return;
33+
}
34+
if (!value.trim()) {
35+
alert('내용을 입력하세요');
36+
return;
37+
}
38+
39+
try {
40+
const id = await createTil(user.uid, value, title);
41+
alert('저장 완료!');
42+
console.log('postId:', id);
43+
44+
router.push(`/write/${id}`);
45+
} catch (e) {
46+
console.error(e);
47+
alert('저장 실패');
48+
}
49+
};
50+
51+
const onChangeTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
52+
setTitle(e.target.value);
53+
};
54+
55+
return (
56+
<div
57+
data-color-mode="light"
58+
className="mx-auto mt-8 flex w-full max-w-5xl flex-col gap-8 rounded-2xl px-4 [&_.w-md-editor]:border-0! [&_.w-md-editor]:bg-transparent! [&_.w-md-editor]:shadow-none [&_.w-md-editor-text]:bg-transparent! [&_.w-md-editor-toolbar]:bg-transparent! [&_.wmde-markdown]:bg-transparent!"
59+
>
60+
<input
61+
type="text"
62+
placeholder="제목을 입력하세요"
63+
onChange={onChangeTitle}
64+
className="py-3 text-3xl focus:ring-0 focus:outline-none"
65+
/>
66+
<MDEditor
67+
value={value}
68+
onChange={(v) => setValue(v ?? '')}
69+
height={700}
70+
preview="live"
71+
textareaProps={{
72+
placeholder: '내용을 입력하세요',
73+
maxLength: 2000,
74+
}}
75+
commandsFilter={(cmd) => {
76+
if (cmd?.name === 'fullscreen') return false;
77+
return cmd;
78+
}}
79+
/>
80+
<SaveButton onClickCancel={onClickCancel} onClickSave={onClickSave} />
81+
</div>
82+
);
83+
};
84+
85+
export default Editor;

components/write/PostDetail.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use client';
2+
3+
import dynamic from 'next/dynamic';
4+
import { useEffect, useState } from 'react';
5+
import { useRouter } from 'next/navigation';
6+
import { auth } from '@/lib/firebase';
7+
import { fetchMyTil } from '@/services/write/til.service';
8+
import type { Til } from '@/services/write/til.service'; // 같은 파일에 타입 넣었다면 거기서 export한 Post 쓰세요
9+
10+
const Markdown = dynamic(
11+
() => import('@uiw/react-md-editor').then((mod) => mod.default.Markdown),
12+
{ ssr: false }
13+
);
14+
15+
const PostDetail = ({ tilId }: { tilId: string }) => {
16+
const router = useRouter();
17+
const [data, setData] = useState<Til | null>(null);
18+
const [loading, setLoading] = useState(true);
19+
20+
useEffect(() => {
21+
const unsub = auth.onAuthStateChanged(async (user) => {
22+
if (!user) {
23+
router.replace('/landing');
24+
return;
25+
}
26+
27+
const post = await fetchMyTil(user.uid, tilId);
28+
setData(post);
29+
setLoading(false);
30+
});
31+
32+
return () => unsub();
33+
}, [tilId, router]);
34+
35+
if (loading) return <div className="min-h-screen p-6">로딩중...</div>;
36+
if (!data) return <div className="min-h-screen p-6">글이 없습니다.</div>;
37+
38+
return (
39+
<div className="mx-auto min-h-screen max-w-3xl p-6" data-color-mode="light">
40+
<h1 className="mb-4 text-xl font-semibold">
41+
{data.title || '제목 없음'}
42+
</h1>
43+
<Markdown source={data.content} />
44+
</div>
45+
);
46+
};
47+
48+
export default PostDetail;

components/write/SaveButton.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
type ButtonsProps = {
2+
onClickCancel: () => void;
3+
onClickSave: () => void;
4+
};
5+
6+
const SaveButton = ({ onClickCancel, onClickSave }: ButtonsProps) => {
7+
return (
8+
<div className="mx-auto flex h-10 w-3xs gap-6">
9+
<button
10+
onClick={onClickSave}
11+
type="button"
12+
className="bg-primary flex-1 rounded-xl py-4 text-center text-base leading-0 font-semibold text-white shadow-sm transition hover:bg-violet-500 active:bg-violet-700 disabled:cursor-not-allowed disabled:opacity-60"
13+
>
14+
저장
15+
</button>
16+
17+
<button
18+
onClick={onClickCancel}
19+
type="button"
20+
className="flex-1 rounded-xl border border-slate-300 bg-none py-4 text-center text-base leading-0 font-semibold text-slate-700 transition hover:bg-slate-200 active:bg-slate-300 disabled:cursor-not-allowed disabled:opacity-60"
21+
>
22+
취소하기
23+
</button>
24+
</div>
25+
);
26+
};
27+
28+
export default SaveButton;

0 commit comments

Comments
 (0)