dh_demo

DreamHanks demo project
git clone git://git.lair.cx/dh_demo
Log | Files | Refs | README

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:
Mcomponents/layout/Header.module.css | 4+---
Acomponents/layout/Header.module.css.map | 2++
Acomponents/layout/Header.module.scss | 8++++++++
Mcomponents/layout/Header.tsx | 43+++++++++++++++++++++++++++++++++++++------
Mlib/error_codes.ts | 3++-
Mlib/models/wiki_info.ts | 41++++++++++++++++++++++++++++++++++++++++-
Rpages/api/users/[id].ts -> pages/api/users/[id]/index.ts | 0
Apages/api/users/[id]/wikis.ts | 35+++++++++++++++++++++++++++++++++++
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) + } +}