commit b1d2a355ce4633a40e07bb4e0204eab1e7847252
parent 9595ca826f13e3f453039b3513765db6b9af905e
Author: Yongbin Kim <iam@yongbin.kim>
Date: Fri, 20 Jan 2023 13:32:02 +0900
feat: 헤더 메뉴에 위키 목록 출력하는 기능 추가
Signed-off-by: Yongbin Kim <iam@yongbin.kim>
Diffstat:
8 files changed, 125 insertions(+), 11 deletions(-)
diff --git a/components/layout/Header.module.css b/components/layout/Header.module.css
@@ -1,3 +1 @@
-.header {
-
-}
+.wiki-list{max-height:10rem;overflow-y:auto}/*# sourceMappingURL=Header.module.css.map */
diff --git a/components/layout/Header.module.css.map b/components/layout/Header.module.css.map
@@ -0,0 +1 @@
+{"version":3,"sourceRoot":"","sources":["Header.module.scss"],"names":[],"mappings":"AAIA,WACE,iBACA","file":"Header.module.css"}
+\ No newline at end of file
diff --git a/components/layout/Header.module.scss b/components/layout/Header.module.scss
@@ -0,0 +1,8 @@
+.header {
+
+}
+
+.wiki-list {
+ max-height: 10rem;
+ overflow-y: auto;
+}
diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx
@@ -5,6 +5,7 @@ import NavbarDropdownLabel from '@/components/layout/NavbarDropdownLabel'
import NavbarItem from '@/components/layout/NavbarItem'
import classNames from '@/lib/classnames'
import { UserProfile } from '@/lib/models/user_profile'
+import { AccessTokenPayload } from '@/lib/security/token'
import { useEffect, useState } from 'react'
import styles from './Header.module.css'
@@ -31,7 +32,7 @@ export default function Header () {
return (
<header {...classNames(styles.header)}>
<Navbar>
- {userProfile == null ? (
+ {userProfile == null || token == null ? (
<>
<NavbarItem href={'/users/login'}>Login</NavbarItem>
<NavbarItem href={'/users/signup'}>Sign up</NavbarItem>
@@ -45,11 +46,7 @@ export default function Header () {
<hr />
- <NavbarDropdownLabel>내 위키</NavbarDropdownLabel>
-
- <NavbarItem href={`/wiki/test`}>
- 테스트
- </NavbarItem>
+ <WikiLinkList token={token} />
<hr />
@@ -63,3 +60,37 @@ export default function Header () {
</header>
)
}
+
+interface WikiListProps {
+ token: AccessTokenPayload
+}
+
+function WikiLinkList (props: WikiListProps) {
+ const [wikis, setWikis] = useState<Array<{ title: string; slug: string }>>([])
+
+ useEffect(() => {
+ fetch(`/api/users/${props.token.uid}/wikis?format=links`, {
+ method: 'GET',
+ })
+ .then(resp => resp.json())
+ .then(data => setWikis(data))
+ }, [props.token?.uid])
+
+ return (
+ <>
+ <NavbarDropdownLabel>내 위키</NavbarDropdownLabel>
+
+ <div className={styles['wiki-list']}>
+ {wikis.map(wiki => (
+ <NavbarItem key={wiki.slug} href={`/wiki/${wiki.slug}`}>
+ {wiki.title}
+ </NavbarItem>
+ ))}
+
+ <NavbarItem href={`/wiki/new`}>
+ 새 위키 만들기
+ </NavbarItem>
+ </div>
+ </>
+ )
+}
diff --git a/lib/error_codes.ts b/lib/error_codes.ts
@@ -8,4 +8,5 @@ export const
ERR_CODE_INVALID_TITLE = 'invalid_title',
ERR_CODE_METHOD_NOT_ALLOWED = 'method_not_allowed',
ERR_CODE_NOT_FOUND = 'not_found',
- ERR_CODE_UNAUTHORIZED = 'unauthorized'
+ ERR_CODE_UNAUTHORIZED = 'unauthorized',
+ ERR_CODE_USER_ID_REQUIRED = 'user_id_required'
diff --git a/lib/models/wiki_info.ts b/lib/models/wiki_info.ts
@@ -1,5 +1,5 @@
import db from '@/lib/db'
-import { OkPacket } from 'mysql2'
+import { OkPacket, RowDataPacket } from 'mysql2'
export interface WikiInfo {
id: number
@@ -28,3 +28,42 @@ export async function createWiki (
return true
}
+
+const SQL_LIST_WIKI_LINKS_BY_OWNER_ID = `
+ select slug, title
+ from wikis
+ where owner_id = ?
+`
+
+export async function listWikiLinksByOwnerId (ownerId: number): Promise<Array<{ slug: string, title: string }>> {
+ const [rows] = await db.query<RowDataPacket[]>({
+ sql: SQL_LIST_WIKI_LINKS_BY_OWNER_ID,
+ }, [ownerId])
+
+ return rows.map(row => ({
+ slug: row[0],
+ title: row[1],
+ }))
+}
+
+const SQL_LIST_WIKI_BY_OWNER_ID = `
+ select id, owner_id, slug, title, description, created_at, updated_at
+ from wikis
+ where owner_id = ?
+`
+
+export async function listWikiByOwnerId (ownerId: number): Promise<WikiInfo[]> {
+ const [rows] = await db.query<RowDataPacket[]>({
+ sql: SQL_LIST_WIKI_BY_OWNER_ID,
+ }, [ownerId])
+
+ return rows.map(row => ({
+ id: row[0],
+ ownerId: row[1],
+ slug: row[2],
+ title: row[3],
+ description: row[4],
+ createdAt: row[5],
+ updatedAt: row[6],
+ }))
+}
diff --git a/pages/api/users/[id].ts b/pages/api/users/[id]/index.ts
diff --git a/pages/api/users/[id]/wikis.ts b/pages/api/users/[id]/wikis.ts
@@ -0,0 +1,35 @@
+import { ERR_INTERNAL, ERR_INVALID_REQUEST, ERR_METHOD_NOT_ALLOWED } from '@/lib/apierror'
+import { listWikiByOwnerId, listWikiLinksByOwnerId } from '@/lib/models/wiki_info'
+import { NextApiRequest, NextApiResponse } from 'next'
+
+export default async function handler (req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== 'GET') {
+ res.status(405).json(ERR_METHOD_NOT_ALLOWED)
+ return
+ }
+
+ // 쿼리에서 ID 파싱
+ let id: number | typeof req.query.id = req.query.id
+ if (typeof id !== 'string') {
+ res.status(400).json(ERR_INVALID_REQUEST)
+ return
+ }
+
+ try {
+ id = parseInt(id, 10)
+ } catch (e) {
+ res.status(400).json(ERR_INVALID_REQUEST)
+ return
+ }
+
+ try {
+ const out = req.query.format === 'links'
+ ? await listWikiLinksByOwnerId(id)
+ : await listWikiByOwnerId(id)
+
+ res.status(200).json(out)
+ } catch (e) {
+ console.error('listWiki: database error:', e)
+ res.status(500).json(ERR_INTERNAL)
+ }
+}