dh_demo

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

commit e97580122b4ca752c9385f3e49958ae83f26265a
parent 858770e4cb65803ef18230dcc5b34d60a3f711cf
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Sun, 29 Jan 2023 01:11:33 +0900

feat: 편집 내역(logs) 목록 페이지 추가

Signed-off-by: Yongbin Kim <iam@yongbin.kim>

Diffstat:
M.pnp.cjs | 33+++++++++++++++++++++++++++++++++
A.yarn/cache/@types-diff-npm-5.0.2-cc002907d4-8fbc419b5a.zip | 0
A.yarn/cache/diff-npm-5.1.0-d24d222280-c7bf0df7c9.zip | 0
A.yarn/cache/moment-npm-2.29.4-902943305d-0ec3f9c2bc.zip | 0
M.yarn/install-state.gz | 0
Mlib/models/wiki_change.ts | 41++++++++++++++++++++++++++++++++++++++---
Alib/utils/pagination.ts | 11+++++++++++
Mlib/utils/wiki.ts | 3++-
Mpackage.json | 3+++
Apages/logs/[slug]/[...path].tsx | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Myarn.lock | 24++++++++++++++++++++++++
11 files changed, 249 insertions(+), 4 deletions(-)

diff --git a/.pnp.cjs b/.pnp.cjs @@ -33,6 +33,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@testing-library/react", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:13.4.0"],\ ["@testing-library/user-event", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:14.4.3"],\ ["@types/bcrypt", "npm:5.0.0"],\ + ["@types/diff", "npm:5.0.2"],\ ["@types/jest", "npm:29.2.6"],\ ["@types/jsonwebtoken", "npm:9.0.1"],\ ["@types/node", "npm:18.11.18"],\ @@ -41,12 +42,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/react-dom", "npm:18.0.10"],\ ["@types/testing-library__jest-dom", "npm:5.14.5"],\ ["bcrypt", "npm:5.1.0"],\ + ["diff", "npm:5.1.0"],\ ["eslint", "npm:8.32.0"],\ ["eslint-config-next", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:13.1.2"],\ ["handlebars", "npm:4.7.7"],\ ["jest", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:29.3.1"],\ ["jest-environment-jsdom", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:29.3.1"],\ ["jsonwebtoken", "npm:9.0.0"],\ + ["moment", "npm:2.29.4"],\ ["mysql2", "npm:3.0.1"],\ ["nanoevents", "npm:7.0.1"],\ ["nanoid", "npm:4.0.0"],\ @@ -1826,6 +1829,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@types/diff", [\ + ["npm:5.0.2", {\ + "packageLocation": "./.yarn/cache/@types-diff-npm-5.0.2-cc002907d4-8fbc419b5a.zip/node_modules/@types/diff/",\ + "packageDependencies": [\ + ["@types/diff", "npm:5.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/graceful-fs", [\ ["npm:4.1.6", {\ "packageLocation": "./.yarn/cache/@types-graceful-fs-npm-4.1.6-1eadcf742d-c3070ccdc9.zip/node_modules/@types/graceful-fs/",\ @@ -3295,6 +3307,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["diff", [\ + ["npm:5.1.0", {\ + "packageLocation": "./.yarn/cache/diff-npm-5.1.0-d24d222280-c7bf0df7c9.zip/node_modules/diff/",\ + "packageDependencies": [\ + ["diff", "npm:5.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["diff-sequences", [\ ["npm:29.3.1", {\ "packageLocation": "./.yarn/cache/diff-sequences-npm-29.3.1-817e98637b-8edab8c383.zip/node_modules/diff-sequences/",\ @@ -3361,6 +3382,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@testing-library/react", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:13.4.0"],\ ["@testing-library/user-event", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:14.4.3"],\ ["@types/bcrypt", "npm:5.0.0"],\ + ["@types/diff", "npm:5.0.2"],\ ["@types/jest", "npm:29.2.6"],\ ["@types/jsonwebtoken", "npm:9.0.1"],\ ["@types/node", "npm:18.11.18"],\ @@ -3369,12 +3391,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/react-dom", "npm:18.0.10"],\ ["@types/testing-library__jest-dom", "npm:5.14.5"],\ ["bcrypt", "npm:5.1.0"],\ + ["diff", "npm:5.1.0"],\ ["eslint", "npm:8.32.0"],\ ["eslint-config-next", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:13.1.2"],\ ["handlebars", "npm:4.7.7"],\ ["jest", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:29.3.1"],\ ["jest-environment-jsdom", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:29.3.1"],\ ["jsonwebtoken", "npm:9.0.0"],\ + ["moment", "npm:2.29.4"],\ ["mysql2", "npm:3.0.1"],\ ["nanoevents", "npm:7.0.1"],\ ["nanoid", "npm:4.0.0"],\ @@ -6443,6 +6467,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["moment", [\ + ["npm:2.29.4", {\ + "packageLocation": "./.yarn/cache/moment-npm-2.29.4-902943305d-0ec3f9c2bc.zip/node_modules/moment/",\ + "packageDependencies": [\ + ["moment", "npm:2.29.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["ms", [\ ["npm:2.1.2", {\ "packageLocation": "./.yarn/cache/ms-npm-2.1.2-ec0c1512ff-673cdb2c31.zip/node_modules/ms/",\ diff --git a/.yarn/cache/@types-diff-npm-5.0.2-cc002907d4-8fbc419b5a.zip b/.yarn/cache/@types-diff-npm-5.0.2-cc002907d4-8fbc419b5a.zip Binary files differ. diff --git a/.yarn/cache/diff-npm-5.1.0-d24d222280-c7bf0df7c9.zip b/.yarn/cache/diff-npm-5.1.0-d24d222280-c7bf0df7c9.zip Binary files differ. diff --git a/.yarn/cache/moment-npm-2.29.4-902943305d-0ec3f9c2bc.zip b/.yarn/cache/moment-npm-2.29.4-902943305d-0ec3f9c2bc.zip Binary files differ. diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz Binary files differ. diff --git a/lib/models/wiki_change.ts b/lib/models/wiki_change.ts @@ -10,6 +10,11 @@ export interface WikiChange { createdAt: Date } +export interface WikiChangeListItem extends WikiChange { + nickname?: string + size: number +} + const SQL_CREATE_WIKI_CHANGE = ` insert into wiki_changes (page_id, author_id, author_ip, text_id) values (?, ?, ?, ?) @@ -26,17 +31,44 @@ export const createWikiChange = modelBehaviour< return rows.insertId }) -const SQL_GET_WIKI_CHANGES = ` - select id, page_id, author_id, author_ip, text_id, created_at +const SQL_COUNT_WIKI_CHANGES = ` + select count(*) from wiki_changes where page_id = ? +` + +export const countWikiChanges = modelBehaviour< + [pageId: number], + number +>(async (conn, args) => { + const [rows] = await conn.query<RowDataPacket[]>({ + sql: SQL_COUNT_WIKI_CHANGES, + }, args) + + return rows[0][0] +}) + +const SQL_GET_WIKI_CHANGES = ` + select + wc.id, + page_id, + author_id, + author_ip, + text_id, + created_at, + up.nickname, + length(wt.content) + from wiki_changes wc + inner join wiki_texts wt on wc.text_id = wt.id + left join user_profiles up on wc.author_id = up.login_id + where page_id = ? order by created_at desc limit ? offset ? ` export const getWikiChanges = modelBehaviour< [pageId: number, limit: number, offset: number], - WikiChange[] + WikiChangeListItem[] >(async (conn, args) => { const [rows] = await conn.query<RowDataPacket[]>({ sql: SQL_GET_WIKI_CHANGES, @@ -49,5 +81,8 @@ export const getWikiChanges = modelBehaviour< authorIp: row[3], textId: row[4], createdAt: row[5], + + nickname: row[6], + size: row[7], })) }) diff --git a/lib/utils/pagination.ts b/lib/utils/pagination.ts @@ -0,0 +1,11 @@ +import { parseIntOrDefault } from '@/lib/utils/number' +import { GetServerSidePropsContext } from 'next' + +export function getPageQuery ( + queries: GetServerSidePropsContext['query'], + pageSize: number +) { + const page = Math.max(parseIntOrDefault(queries.page, 1), 1) + const offset = (page - 1) * pageSize + return [page, offset] +} diff --git a/lib/utils/wiki.ts b/lib/utils/wiki.ts @@ -1,7 +1,8 @@ import { GetServerSidePropsContext, NextApiRequest } from 'next' +import { NextRouter } from 'next/router' export const getSlugAndPath = ( - context: GetServerSidePropsContext | NextApiRequest + context: GetServerSidePropsContext | NextApiRequest | NextRouter ): [slug: string | null, path: string | null] => { const { slug, path } = context.query if (typeof slug !== 'string' || path == null || !Array.isArray(path)) { diff --git a/package.json b/package.json @@ -13,14 +13,17 @@ "dependencies": { "@next/font": "13.1.2", "@types/bcrypt": "^5.0.0", + "@types/diff": "^5.0.2", "@types/node": "18.11.18", "@types/react": "18.0.26", "@types/react-dom": "18.0.10", "bcrypt": "^5.1.0", + "diff": "^5.1.0", "eslint": "8.32.0", "eslint-config-next": "13.1.2", "handlebars": "^4.7.7", "jsonwebtoken": "^9.0.0", + "moment": "^2.29.4", "mysql2": "^3.0.1", "nanoevents": "^7.0.1", "nanoid": "^4.0.0", diff --git a/pages/logs/[slug]/[...path].tsx b/pages/logs/[slug]/[...path].tsx @@ -0,0 +1,138 @@ +import Title from '@/components/elements/Title' +import Container from '@/components/layout/Container' +import Hero from '@/components/layout/Hero' +import Section from '@/components/layout/Section' +import { Pagination } from '@/components/Pagination' +import { withConnection } from '@/lib/model_helpers' +import { getWikiAndPageACLViaPath } from '@/lib/models/wiki_acl' +import { countWikiChanges, getWikiChanges, WikiChangeListItem } from '@/lib/models/wiki_change' +import { getPageQuery } from '@/lib/utils/pagination' +import { getSlugAndPath } from '@/lib/utils/wiki' +import moment from 'moment' +import { GetServerSideProps } from 'next' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useMemo } from 'react' + +const PAGE_SIZE = 30 + +export interface WikiLogsPageProps { + page: number + count: number + changes: WikiChangeListItem[] +} + +export const getServerSideProps: GetServerSideProps<WikiLogsPageProps> = async (context) => { + const [slug, path] = getSlugAndPath(context) ?? [] + if (slug == null || path == null) { + console.error('slug or path is null', slug, path) + return { notFound: true } + } + + const [page, offset] = getPageQuery(context.query, PAGE_SIZE) + + const result = await withConnection(async (conn) => { + const aclInfo = await getWikiAndPageACLViaPath(conn, [slug, path]) + if (aclInfo == null || aclInfo.pageId == null) { + return null + } + + const count = await countWikiChanges(conn, [aclInfo.pageId]) + const changes = await getWikiChanges(conn, [aclInfo.pageId, PAGE_SIZE, offset]) + + return { count, changes } + }) + if (result == null || result.changes.length === 0) { + return { notFound: true } + } + + return { + props: { + page, + count: result.count, + changes: result.changes + }, + } +} + +export default function WikiLogsPage (props: WikiLogsPageProps) { + const router = useRouter() + const [slug, path] = useMemo(() => getSlugAndPath(router), [router]) + return ( + <> + <Hero> + <Title kind="headline" size="large">편집 기록: {path}</Title> + </Hero> + + <Container> + <Section> + <ul> + {props.changes.map((change, i, changes) => ( + <li key={change.id}> + <Link href={`/wiki/${slug}/${path}?rev=${change.id}`}> + {moment(change.createdAt).format('YYYY년 MM월 DD일 hh시 mm분')} + </Link> + &nbsp;&nbsp; + <AuthorLink + authorId={change.authorId} + authorIp={change.authorIp} + nickname={change.nickname} + /> + &nbsp;&nbsp; + <span>{change.size} bytes</span> + &nbsp;&nbsp; + <SizeDiff + size={change.size} + prev={i + 1 === changes.length ? 0 : changes[i + 1].size} + /> + </li> + ))} + </ul> + </Section> + + <Section> + <Pagination page={props.page} totalCount={props.count} pageSize={PAGE_SIZE}/> + </Section> + </Container> + </> + ) +} + +function AuthorLink (props: { authorId?: number; authorIp?: string; nickname?: string }) { + if (props.authorId != null && props.nickname != null) { + return ( + <Link href={`/users/${props.authorId}`}> + {props.nickname} + </Link> + ) + } + + if (props.authorIp != null) { + return ( + <span>{props.authorIp}</span> + ) + } + + return ( + <span>unknown</span> + ) +} + +function SizeDiff (props: { size: number; prev: number }) { + const diff = props.size - props.prev + if (diff > 0) { + return ( + <span>(+{diff} bytes)</span> + ) + } + + if (diff < 0) { + return ( + <span>(-{diff} bytes)</span> + ) + } + + return ( + <span>(±0 bytes)</span> + ) +} diff --git a/yarn.lock b/yarn.lock @@ -1187,6 +1187,13 @@ __metadata: languageName: node linkType: hard +"@types/diff@npm:^5.0.2": + version: 5.0.2 + resolution: "@types/diff@npm:5.0.2" + checksum: 8fbc419b5aca33f494026bf5f70e026f76367689677ef114f9c078ac738d7dbe96e6dda3fd8290e4a7c35281e2b60b034e3d7e3c968b850cf06a21279e7ddcbe + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.6 resolution: "@types/graceful-fs@npm:4.1.6" @@ -2390,6 +2397,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.1.0": + version: 5.1.0 + resolution: "diff@npm:5.1.0" + checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -2442,6 +2456,7 @@ __metadata: "@testing-library/react": ^13.4.0 "@testing-library/user-event": ^14.4.3 "@types/bcrypt": ^5.0.0 + "@types/diff": ^5.0.2 "@types/jest": ^29.2.6 "@types/jsonwebtoken": ^9.0.1 "@types/node": 18.11.18 @@ -2450,12 +2465,14 @@ __metadata: "@types/react-dom": 18.0.10 "@types/testing-library__jest-dom": ^5.14.5 bcrypt: ^5.1.0 + diff: ^5.1.0 eslint: 8.32.0 eslint-config-next: 13.1.2 handlebars: ^4.7.7 jest: ^29.3.1 jest-environment-jsdom: ^29.3.1 jsonwebtoken: ^9.0.0 + moment: ^2.29.4 mysql2: ^3.0.1 nanoevents: ^7.0.1 nanoid: ^4.0.0 @@ -5078,6 +5095,13 @@ __metadata: languageName: node linkType: hard +"moment@npm:^2.29.4": + version: 2.29.4 + resolution: "moment@npm:2.29.4" + checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e + languageName: node + linkType: hard + "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2"