diff --git a/next.config.js b/next.config.js index 8c71c1be..2eeccdc4 100644 --- a/next.config.js +++ b/next.config.js @@ -1,17 +1,21 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'standalone', - experimental: { - appDir: true, - }, -} - -module.exports = nextConfig + output: 'standalone', + experimental: { + appDir: true, + }, + images: { + remotePatterns: [ + { protocol: 'https', hostname: 's3.ap-northeast-2.amazonaws.com' }, + ], + }, +}; +module.exports = nextConfig; // Injected content via Sentry wizard below -const { withSentryConfig } = require("@sentry/nextjs"); +const { withSentryConfig } = require('@sentry/nextjs'); module.exports = withSentryConfig( module.exports, @@ -22,8 +26,8 @@ module.exports = withSentryConfig( // Suppresses source map uploading logs during build silent: true, - org: "funssion", - project: "funssion-front-dev", + org: 'funssion', + project: 'funssion-front-dev', }, { // For all available options, see: @@ -36,7 +40,7 @@ module.exports = withSentryConfig( transpileClientSDK: true, // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) - tunnelRoute: "/monitoring", + tunnelRoute: '/monitoring', // Hides source maps from generated client bundles hideSourceMaps: true, diff --git a/package-lock.json b/package-lock.json index 060ad874..4f71a91d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,15 +36,19 @@ "eslint-config-next": "13.4.7", "lucide-react": "^0.259.0", "next": "^13.2.4", + "openai-edge": "^1.2.2", "postcss": "8.4.24", "react": "^18.2.0", + "react-calendar": "^4.6.0", "react-dom": "18.2.0", "react-icons": "^4.10.1", + "react-spinners": "^0.13.8", "sonner": "^0.6.2", "tailwind-merge": "^1.13.2", "tailwindcss": "3.3.2", "tailwindcss-animate": "^1.0.6", "tiptap-markdown": "^0.8.1", + "typeit-react": "^2.6.4", "typescript": "5.1.3" }, "devDependencies": { @@ -52,6 +56,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", + "@types/react-test-renderer": "^18.0.0", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", "react-test-renderer": "^18.2.0", @@ -2793,6 +2798,19 @@ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" }, + "node_modules/@types/lodash": { + "version": "4.14.196", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz", + "integrity": "sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==" + }, + "node_modules/@types/lodash.memoize": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/lodash.memoize/-/lodash.memoize-4.1.7.tgz", + "integrity": "sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/markdown-it": { "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", @@ -2845,6 +2863,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-test-renderer": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz", + "integrity": "sha512-C7/5FBJ3g3sqUahguGi03O79b8afNeSD6T8/GU50oQrJCU0bVCCGQHaGKUbg2Ce8VQEEqTw8/HiS6lXHHdgkdQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -3151,6 +3178,14 @@ "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "peer": true }, + "node_modules/@wojtekmaj/date-utils": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.0.tgz", + "integrity": "sha512-0mq88lCND6QiffnSDWp+TbOxzJSwy2V/3XN+HwWZ7S2n19QAgR5dy5hRVhlECXvQIq2r+VcblBu+S9V+yMcxXw==", + "funding": { + "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -5275,6 +5310,18 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-user-locale": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.3.0.tgz", + "integrity": "sha512-I3rQvAUwu2nauRD9YyQBSXVFJZixNouwA+eZld51Sn4Pn0N1qFbgcgOi/nPigJPQlNY519mT95fiSPRgflQiTA==", + "dependencies": { + "@types/lodash.memoize": "^4.1.7", + "lodash.memoize": "^4.1.1" + }, + "funding": { + "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" + } + }, "node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -7317,8 +7364,7 @@ "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -7928,6 +7974,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai-edge": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/openai-edge/-/openai-edge-1.2.2.tgz", + "integrity": "sha512-C3/Ao9Hkx5uBPv9YFBpX/x59XMPgPUU4dyGg/0J2sOJ7O9D98kD+lfdOc7v/60oYo5xzMGct80uFkYLH+X2qgw==", + "engines": { + "node": ">=18" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -8623,6 +8677,39 @@ "node": ">=0.10.0" } }, + "node_modules/react-calendar": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-4.6.0.tgz", + "integrity": "sha512-GJ6ZipKMQmlK666t+0hgmecu6WHydEnMWJjKdEkUxW6F471hiM5DkbWXkfr8wlAg9tc9feNCBhXw3SqsPOm01A==", + "dependencies": { + "@wojtekmaj/date-utils": "^1.1.3", + "clsx": "^2.0.0", + "get-user-locale": "^2.2.1", + "prop-types": "^15.6.0", + "tiny-warning": "^1.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-calendar/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -8661,6 +8748,15 @@ "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-spinners": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", + "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-test-renderer": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", @@ -9589,6 +9685,11 @@ "node": ">=10" } }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tippy.js": { "version": "6.3.7", "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", @@ -9810,6 +9911,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typeit": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/typeit/-/typeit-8.7.1.tgz", + "integrity": "sha512-Bx/O4NMz10NWh9FWYtVwV4XwGHF9UDJfpCZPJRtw2/oUcahFAStU8J0t19aroPfTV6s1UlS5ICoqilOqmEnh2Q==", + "hasInstallScript": true + }, + "node_modules/typeit-react": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/typeit-react/-/typeit-react-2.6.4.tgz", + "integrity": "sha512-ZFx7MYYirvQ1P6qvzN+ONMOWzkktuw2skBln/GQc5xpzqfSA48J2Fn5De/KtVkYbWOuLfKBPTwzJTH+clbAsAA==", + "dependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0", + "typeit": "^8.7.1" + } + }, "node_modules/typescript": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", diff --git a/package.json b/package.json index f931319c..19d87961 100644 --- a/package.json +++ b/package.json @@ -39,15 +39,19 @@ "eslint-config-next": "13.4.7", "lucide-react": "^0.259.0", "next": "^13.2.4", + "openai-edge": "^1.2.2", "postcss": "8.4.24", "react": "^18.2.0", + "react-calendar": "^4.6.0", "react-dom": "18.2.0", "react-icons": "^4.10.1", + "react-spinners": "^0.13.8", "sonner": "^0.6.2", "tailwind-merge": "^1.13.2", "tailwindcss": "3.3.2", "tailwindcss-animate": "^1.0.6", "tiptap-markdown": "^0.8.1", + "typeit-react": "^2.6.4", "typescript": "5.1.3" }, "devDependencies": { @@ -55,6 +59,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", + "@types/react-test-renderer": "^18.0.0", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", "react-test-renderer": "^18.2.0", diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts new file mode 100644 index 00000000..42674225 --- /dev/null +++ b/src/app/api/generate/route.ts @@ -0,0 +1,68 @@ +import { Configuration, OpenAIApi } from 'openai-edge'; +import { OpenAIStream, StreamingTextResponse } from 'ai'; + +const config = new Configuration({ + apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY, +}); +const openai = new OpenAIApi(config); + +export const runtime = 'edge'; + +export async function POST(req: Request): Promise { + // if ( + // // process.env.NODE_ENV != 'development' && + // // process.env.KV_REST_API_URL && + // // process.env.KV_REST_API_TOKEN + // false + // ) { + // const ip = req.headers.get('x-forwarded-for'); + // const ratelimit = new Ratelimit({ + // redis: kv, + // limiter: Ratelimit.slidingWindow(50, '1 d'), + // }); + + // const { success, limit, reset, remaining } = await ratelimit.limit( + // `novel_ratelimit_${ip}` + // ); + + // if (!success) { + // return new Response('You have reached your request limit for the day.', { + // status: 429, + // headers: { + // 'X-RateLimit-Limit': limit.toString(), + // 'X-RateLimit-Remaining': remaining.toString(), + // 'X-RateLimit-Reset': reset.toString(), + // }, + // }); + // } + // } + let { prompt } = await req.json(); + const response = await openai.createChatCompletion({ + model: 'gpt-3.5-turbo', + messages: [ + { + role: 'system', + content: + 'You are an AI writing assistant that continues existing text based on context from prior text. ' + + 'Give more weight/priority to the later characters than the beginning ones. ' + + 'Limit your response to no more than 400 characters, but make sure to construct complete sentences.', + }, + { + role: 'user', + content: prompt, + }, + ], + temperature: 0.7, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0, + stream: true, + n: 1, + }); + + // Convert the response into a friendly text-stream + const stream = OpenAIStream(response); + + // Respond with the stream + return new StreamingTextResponse(stream); +} diff --git a/src/app/create/memo/[slug]/page.tsx b/src/app/create/memo/[slug]/page.tsx index 5c90a665..b588e599 100644 --- a/src/app/create/memo/[slug]/page.tsx +++ b/src/app/create/memo/[slug]/page.tsx @@ -1,4 +1,5 @@ import EditorForm from '@/components/EditorForm'; +import LayoutWrapper from '@/components/shared/LayoutWrapper'; import { getMemoById } from '@/service/memos'; type Props = { @@ -11,13 +12,15 @@ export default async function CreateMemoPage({ params: { slug } }: Props) { const { memoTitle, memoColor, memoText } = await getMemoById(slug); return ( - + + + ); } diff --git a/src/app/create/memo/page.tsx b/src/app/create/memo/page.tsx index 53e98259..ccdbfe2e 100644 --- a/src/app/create/memo/page.tsx +++ b/src/app/create/memo/page.tsx @@ -1,4 +1,5 @@ import EditorForm from '@/components/EditorForm'; +import LayoutWrapper from '@/components/shared/LayoutWrapper'; import { Metadata } from 'next'; export const metadata: Metadata = { @@ -7,5 +8,9 @@ export const metadata: Metadata = { }; export default function CreateMemoPage() { - return ; + return ( + + + + ); } diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fea..b8ad6b31 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/landing/page.tsx b/src/app/landing/page.tsx new file mode 100644 index 00000000..5aa62ea8 --- /dev/null +++ b/src/app/landing/page.tsx @@ -0,0 +1,45 @@ +'use client'; + +import Link from 'next/link'; +import { useState } from 'react'; +import TypeIt from 'typeit-react'; + +export default function LandingPage() { + const [state, setState] = useState(false); + return ( +
+

+ { + setState(true); + }, + }} + > + 개발 기록을 쉽고 즐겁게, 인포럼 + +

+
+

+ 자동 텍스트 생성 기능을 통해 글을 쉽고 빠르게 작성하세요 +
+ 파스텔톤 메모에 개발 기록을 간단하게 작성해보세요 +

+ + + +
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cd762ea2..ce464f9b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,9 +1,9 @@ import '@/styles/globals.css'; import '@/styles/prosemirror.css'; -import { Inter } from 'next/font/google'; import Providers from './providers'; - -const inter = Inter({ subsets: ['latin'] }); +import { pretendard } from '@/styles/fonts'; +import ModalProvider from '@/context/ModalProvider'; +import Modal from '@/components/Modal'; export const metadata = { title: 'Inforum', @@ -16,9 +16,12 @@ export default async function RootLayout({ children: React.ReactNode; }) { return ( - - - {children} + + + + {children} + + ); diff --git a/src/app/loading.tsx b/src/app/loading.tsx new file mode 100644 index 00000000..c63a116e --- /dev/null +++ b/src/app/loading.tsx @@ -0,0 +1,9 @@ +import { BeatLoader } from 'react-spinners'; + +export default function Loading() { + return ( +
+ +
+ ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index cfdc8476..192f5760 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -9,15 +9,26 @@ export const metadata: Metadata = { export default function LoginPage() { return ( -
- +
+ 메인으로

Inforum

- - 회원가입 - +
+

+ 계정이 없으신가요? +

+ + 회원가입하기 + +
); } diff --git a/src/app/me/[slug]/page.tsx b/src/app/me/[slug]/page.tsx index b2fc4123..5ea1e5a0 100644 --- a/src/app/me/[slug]/page.tsx +++ b/src/app/me/[slug]/page.tsx @@ -1,8 +1,11 @@ import Header from '@/components/shared/Header'; -import History from '@/components/history/History'; import MemosGrid from '@/components/memo/MemosGrid'; import Profile from '@/components/shared/Profile'; -import { getMemosByUserId, getUserInfo } from '@/service/memos'; +import { getHistory, getMemosByUserId } from '@/service/me'; +import LayoutWrapper from '@/components/shared/LayoutWrapper'; +import History from '@/components/history/History'; +import { getUserInfo2 } from '@/service/auth'; +import SettingBtns from '@/components/me/SettingBtns'; type Props = { params: { @@ -12,29 +15,39 @@ type Props = { export default async function MePage({ params: { slug } }: Props) { const memos = await getMemosByUserId(slug); - const userInfo = await getUserInfo(slug); - + const userInfo = await getUserInfo2(slug); + const history = await getHistory( + slug, + new Date().getFullYear(), + new Date().getMonth() + 1, + true + ); return ( -
+
-
-
- - -
-
-

My Memos

- -
-
-
+ +
+
+ + + +
+
+

+ My Memos +

+ +
+
+
+
); } export async function generateMetadata({ params }: Props) { - const { userName } = await getUserInfo(params.slug); + const { nickname } = await getUserInfo2(params.slug); return { - title: userName, + title: nickname, }; } diff --git a/src/app/me/setting/[slug]/page.tsx b/src/app/me/setting/[slug]/page.tsx new file mode 100644 index 00000000..928862a7 --- /dev/null +++ b/src/app/me/setting/[slug]/page.tsx @@ -0,0 +1,21 @@ +import MyInfoForm from '@/components/MyInfoForm'; +import { getUserInfo2 } from '@/service/auth'; +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; + +type Props = { + params: { + slug: number; + }; +}; + +export default async function MySettingPage({ params: { slug } }: Props) { + const userInfo = await getUserInfo2(slug); + + return ( +
+

Inforum

+ +
+ ); +} diff --git a/src/app/memos/[slug]/page.tsx b/src/app/memos/[slug]/page.tsx index a139788f..c4df1767 100644 --- a/src/app/memos/[slug]/page.tsx +++ b/src/app/memos/[slug]/page.tsx @@ -1,6 +1,7 @@ import Header from '@/components/shared/Header'; import MemoViewer from '@/components/memo/MemoViewer'; import { getMemoById } from '@/service/memos'; +import LayoutWrapper from '@/components/shared/LayoutWrapper'; type Props = { params: { @@ -9,19 +10,24 @@ type Props = { }; export default async function MemoPage({ params: { slug } }: Props) { - const { memoTitle, memoColor, memoText, userId } = await getMemoById(slug); + const { memoTitle, memoColor, memoText, authorId, likes } = await getMemoById( + slug + ); return ( - <> +
- - + + + +
); } diff --git a/src/app/memos/page.tsx b/src/app/memos/page.tsx index 22f38bb7..d4be46fc 100644 --- a/src/app/memos/page.tsx +++ b/src/app/memos/page.tsx @@ -1,3 +1,19 @@ -export default function MemosPage() { - return <>Memos!; +import MemosGrid from '@/components/memo/MemosGrid'; +import Footer from '@/components/shared/Footer'; +import Header from '@/components/shared/Header'; +import LayoutWrapper from '@/components/shared/LayoutWrapper'; +import { getMemos } from '@/service/memos'; + +export default async function MemosPage() { + const memos = await getMemos(); + + return ( +
+
+ + + +
+
+ ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 977023ef..6fd11c8f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,14 +1,45 @@ -import Header from '@/components/shared/Header'; -import MemosGrid from '@/components/memo/MemosGrid'; -import { getMemos } from '@/service/memos'; +'use client'; -export default async function HomePage() { - const memos = await getMemos(); +import Link from 'next/link'; +import { useState } from 'react'; +import TypeIt from 'typeit-react'; +export default function LandingPage() { + const [state, setState] = useState(false); return ( -
-
- -
+
+

+ { + setState(true); + }, + }} + > + 개발 기록을 쉽고 즐겁게, 인포럼 + +

+
+

+ 자동 텍스트 생성 기능을 통해 글을 쉽고 빠르게 작성하세요 +
+ 파스텔톤 메모에 개발 기록을 간단하게 작성해보세요 +

+ + + +
+
); } diff --git a/src/app/providers.tsx b/src/app/providers.tsx index ca28d495..3c484dac 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -52,7 +52,7 @@ export default function Providers({ children }: { children: ReactNode }) {
{children} diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index c1af5901..22444381 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -9,15 +9,26 @@ export const metadata: Metadata = { export default function SignupPage() { return ( -
- +
+ 메인으로

Inforum

- - 로그인 - +
+

+ 계정이 이미 있나요? +

+ + 로그인하기 + +
); } diff --git a/src/app/signup/setting/[slug]/page.tsx b/src/app/signup/setting/[slug]/page.tsx new file mode 100644 index 00000000..3b66b94d --- /dev/null +++ b/src/app/signup/setting/[slug]/page.tsx @@ -0,0 +1,23 @@ +import MyInfoForm from '@/components/MyInfoForm'; +import { redirect } from 'next/navigation'; +import { headers } from 'next/headers'; + +type Props = { + params: { + slug: number; + }; +}; + +export default function SignupSettingPage({ params: { slug } }: Props) { + const headersList = headers(); + const referer = headersList.get('referer'); + + if (referer === null) redirect('/memos'); + + return ( +
+

Inforum

+ +
+ ); +} diff --git a/src/assets/icons/edit.svg b/src/assets/icons/edit.svg new file mode 100644 index 00000000..78fc7484 --- /dev/null +++ b/src/assets/icons/edit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/heart_empty.svg b/src/assets/icons/heart_empty.svg new file mode 100644 index 00000000..075b336d --- /dev/null +++ b/src/assets/icons/heart_empty.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/heart_fill.svg b/src/assets/icons/heart_fill.svg new file mode 100644 index 00000000..7620445f --- /dev/null +++ b/src/assets/icons/heart_fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/icon_error.svg b/src/assets/icons/icon_error.svg new file mode 100644 index 00000000..d65ded2a --- /dev/null +++ b/src/assets/icons/icon_error.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icons/more.svg b/src/assets/icons/more.svg new file mode 100644 index 00000000..b56f4950 --- /dev/null +++ b/src/assets/icons/more.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/profile.svg b/src/assets/profile.svg new file mode 100644 index 00000000..bc2bbec2 --- /dev/null +++ b/src/assets/profile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/EditorForm.tsx b/src/components/EditorForm.tsx index 706ef27b..7c782e23 100644 --- a/src/components/EditorForm.tsx +++ b/src/components/EditorForm.tsx @@ -6,9 +6,13 @@ import { TiptapExtensions } from '@/components/ui/editor/extensions'; import { TiptapEditorProps } from '@/components/ui/editor/props'; import { useEditor } from '@tiptap/react'; import { useRouter } from 'next/navigation'; -import { useState } from 'react'; +import { useContext, useEffect, useRef, useState } from 'react'; import BlueBtn from './shared/BlueBtn'; import { createOrUpdateMemo } from '@/service/memos'; +import { getPrevText } from '@/lib/editor'; +import { useCompletion } from 'ai/react'; +import { getDescription } from '@/service/description'; +import { ModalContext } from '@/context/ModalProvider'; type Props = { preTitle?: string; @@ -26,57 +30,135 @@ export default function EditorForm({ memoId, }: Props) { const router = useRouter(); + const { open } = useContext(ModalContext); const editor = useEditor({ extensions: TiptapExtensions, editorProps: TiptapEditorProps, content: preContent, - // onUpdate: e => { - // setSaveStatus('Unsaved'); - // const selection = e.editor.state.selection; - // const lastTwo = getPrevText(e.editor, { - // chars: 2, - // }); - // if (lastTwo === '++' && !isLoading) { - // e.editor.commands.deleteRange({ - // from: selection.from - 2, - // to: selection.from, - // }); - // complete( - // getPrevText(e.editor, { - // chars: 5000, - // }) - // ); - // // complete(e.editor.storage.markdown.getMarkdown()); - // va.track('Autocomplete Shortcut Used'); - // } else { - // debouncedUpdates(e); - // } - // }, + onUpdate: (e) => { + // setSaveStatus('Unsaved'); + const selection = e.editor.state.selection; + const lastTwo = getPrevText(e.editor, { + chars: 2, + }); + if (lastTwo === '++' && !isLoading) { + e.editor.commands.deleteRange({ + from: selection.from - 2, + to: selection.from, + }); + complete( + getPrevText(e.editor, { + chars: 5000, + }) + ); + // complete(e.editor.storage.markdown.getMarkdown()); + // va.track('Autocomplete Shortcut Used'); + } else { + // debouncedUpdates(e); + } + }, + }); + + const { complete, completion, isLoading, stop } = useCompletion({ + id: 'novel', + api: '/api/generate', + onFinish: (_prompt, completion) => { + editor?.commands.setTextSelection({ + from: editor.state.selection.from - completion.length, + to: editor.state.selection.from, + }); + }, + onError: (err) => { + // toast.error(err.message); + // if (err.message === 'You have reached your request limit for the day.') { + // va.track('Rate Limit Reached'); + // } + }, }); + + const prev = useRef(''); + + // Insert chunks of the generated text + useEffect(() => { + const diff = completion.slice(prev.current.length); + prev.current = completion; + editor?.commands.insertContent(diff); + }, [isLoading, editor, completion]); + + useEffect(() => { + // if user presses escape or cmd + z and it's loading, + // stop the request, delete the completion, and insert back the "++" + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' || (e.metaKey && e.key === 'z')) { + stop(); + if (e.key === 'Escape') { + editor?.commands.deleteRange({ + from: editor.state.selection.from - completion.length, + to: editor.state.selection.from, + }); + } + editor?.commands.insertContent('++'); + } + }; + const mousedownHandler = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + stop(); + if (window.confirm('AI writing paused. Continue?')) { + complete(editor?.getText() || ''); + } + }; + if (isLoading) { + document.addEventListener('keydown', onKeyDown); + window.addEventListener('mousedown', mousedownHandler); + } else { + document.removeEventListener('keydown', onKeyDown); + window.removeEventListener('mousedown', mousedownHandler); + } + return () => { + document.removeEventListener('keydown', onKeyDown); + window.removeEventListener('mousedown', mousedownHandler); + }; + }, [stop, isLoading, editor, complete, completion.length]); + const [title, setTitle] = useState(preTitle); const [selectedColor, setSelectedColor] = useState(preColor); - const handleBtnClick = () => + const handleBtnClick = () => { + if (title === '') { + alert('제목을 작성해주세요!'); + return; + } + + const memoText = JSON.stringify(editor?.getJSON()); + + if (!memoText.includes('text')) { + alert('내용을 작성해주세요!'); + return; + } + + const memoDescription = getDescription(memoText); createOrUpdateMemo( `${process.env.NEXT_PUBLIC_SERVER_IP_ADDRESS_SECURE}/memos${ !alreadyExists ? `/${memoId}` : '' }`, { memoTitle: title, - memoDescription: 'test description', - memoText: JSON.stringify(editor?.getJSON()), + memoDescription, + memoText, memoColor: selectedColor, } ).then(() => { - if (alreadyExists) router.push('/'); + if (alreadyExists) router.push('/memos'); else router.push(`/memos/${memoId}`); router.refresh(); }); + }; const handleColorClick = (color: string) => setSelectedColor(color); return (
setTitle(e.target.value)} - className="w-full outline-none text-4xl px-4 py-3 bg-transparent font-bold mt-2 border-t-2 border-gray-400" + className="w-full outline-none text-2xl sm:text-4xl px-4 py-3 bg-transparent font-bold mt-2 border-t-2 border-gray-400" autoFocus /> {/*

tag

*/} @@ -107,7 +189,11 @@ export default function EditorForm({ diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index 6d3cd33d..3ba8a0d9 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -4,9 +4,11 @@ import { login } from '@/service/auth'; import { LoginFormData } from '@/types'; import { useRouter } from 'next/navigation'; import { ChangeEvent, FormEvent, useState } from 'react'; +import BlueBtn from './shared/BlueBtn'; +import PromptgMessage from './shared/PromptMessage'; const INPUT_STYLE = - 'text-lg border-2 my-2 py-2 px-4 rounded-lg bg-soma-grey-20 border-soma-grey-30'; + 'text-lg border-2 my-2 py-2 px-4 rounded-lg bg-soma-grey-20 border-soma-grey-30 text-sm sm:text-base'; export default function LoginForm() { const router = useRouter(); @@ -15,6 +17,10 @@ export default function LoginForm() { pw: '', }); + const [messageText, setMessageText] = useState(''); + const [messageType, setMessageType] = useState(false); // true : 성공 메시지 false : 경고 메시지 + const [isMessageVisible, setIsMessageVisible] = useState(false); + const handleChange = (e: ChangeEvent) => { const { value, name } = e.target; setLoginData((info) => ({ ...info, [name]: value })); @@ -22,14 +28,33 @@ export default function LoginForm() { const handleSubmit = (e: FormEvent) => { e.preventDefault(); - login({ user_email: loginData.email, user_pw: loginData.pw }).then(() => { - router.push('/'); - router.refresh(); - }); + login({ user_email: loginData.email, user_pw: loginData.pw }).then( + (data) => { + showMessage(data.message, data.isSuccess); + if (data.isSuccess) { + router.push('/memos'); + router.refresh(); + } + } + ); + }; + + const showMessage = (text: string, type: boolean) => { + setMessageText(text); + setMessageType(type); + setIsMessageVisible(true); + setTimeout(() => { + setIsMessageVisible(false); + }, 2000); }; return (
+ - + {}} extraStyle="my-5" /> ); } diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 00000000..117ef4d3 --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { ModalContext } from '@/context/ModalProvider'; +import { useContext } from 'react'; +import BlueBtn from './shared/BlueBtn'; +import BlueBtn2 from './shared/BlueBtn2'; + +export default function Modal() { + const { isOpen, close, onSuccess, modalText } = useContext(ModalContext); + + return ( + isOpen && ( +
+ close()} /> +
+

{modalText}

+
+ { + close(); + onSuccess(); + }} + /> + close()} /> +
+
+
+ ) + ); +} + +function Overay({ onClick }: { onClick: () => void }) { + return ( +
+ ); +} diff --git a/src/components/MyInfoForm.tsx b/src/components/MyInfoForm.tsx new file mode 100644 index 00000000..482653c1 --- /dev/null +++ b/src/components/MyInfoForm.tsx @@ -0,0 +1,136 @@ +'use client'; + +import Image from 'next/image'; +import { + ChangeEvent, + FormEvent, + MutableRefObject, + useRef, + useState, +} from 'react'; + +import profileImage from '../assets/profile.svg'; +import editIcon from '../assets/icons/edit.svg'; +import BlueBtn from './shared/BlueBtn'; +import BlueBtn2 from './shared/BlueBtn2'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { registerUserInfo, updateUserInfo } from '@/service/auth'; +import { UserInfo2 } from '@/types'; + +type Props = { + userId: number; + userInfo?: UserInfo2; + isSignup: boolean; +}; + +export default function MyInfoForm({ userId, userInfo, isSignup }: Props) { + const router = useRouter(); + + const [imageFile, setImageFile] = useState(null); + const [imageUrl, setImageUrl] = useState( + userInfo?.profileImageFilePath ?? '' + ); + const [intro, setIntro] = useState(userInfo?.introduce ?? ''); + const [tags, setTags] = useState(userInfo?.tags ?? ''); + + const fileInput = useRef() as MutableRefObject; + + const handleFileChange = (e: ChangeEvent) => { + if (e.target.files !== null) { + const file = e.target.files[0]; + const url = window.URL.createObjectURL(file); + setImageFile(file); + setImageUrl(url); + } + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + isSignup + ? registerUserInfo( + userId, + imageFile, + intro, + tags, + imageUrl === '' && imageFile === null ? 'true' : 'false' + ).then(() => { + router.push('/login'); + }) + : updateUserInfo( + userId, + imageFile, + intro, + tags, + imageUrl === '' && imageFile === null ? 'true' : 'false' + ).then(() => { + router.push(`/me/${userId}`); + }); + }; + + return ( +
+
+ + +
+ + +
+ +