diff --git a/prisma/migrations/20241216085248_is_anonymous/migration.sql b/prisma/migrations/20241216085248_is_anonymous/migration.sql new file mode 100644 index 0000000..474e93d --- /dev/null +++ b/prisma/migrations/20241216085248_is_anonymous/migration.sql @@ -0,0 +1,14 @@ +/* + * Migration + */ +-- AlterTable +ALTER TABLE "question" ADD COLUMN "isAnonymous" BOOLEAN; + +UPDATE "question" +SET + "isAnonymous" = CASE + WHEN "questioner" IS NULL THEN TRUE + ELSE FALSE + END; + +ALTER TABLE "question" ALTER COLUMN "isAnonymous" SET NOT NULL; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e652fab..e1032f4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -76,6 +76,7 @@ model question { questionee profile @relation(fields: [questioneeHandle], references: [handle], onDelete: Cascade) questioneeHandle String questionedAt DateTime @default(now()) + isAnonymous Boolean /// is Anonymous question? @@index([questioneeHandle]) } diff --git a/src/app/_components/answer.tsx b/src/app/_components/answer.tsx index b1e7100..deada8d 100644 --- a/src/app/_components/answer.tsx +++ b/src/app/_components/answer.tsx @@ -3,7 +3,7 @@ import Link from 'next/link'; import { Dispatch, RefObject, SetStateAction, useEffect, useState } from 'react'; import NameComponents from './NameComponents'; -import { AnswerWithProfileDto } from '../_dto/Answers.dto'; +import { AnswerWithProfileDto } from '../_dto/answers/Answers.dto'; import { userProfileDto } from '../_dto/fetch-profile/Profile.dto'; import { useParams } from 'next/navigation'; diff --git a/src/app/_components/question.tsx b/src/app/_components/question.tsx index 27c7039..bf29443 100644 --- a/src/app/_components/question.tsx +++ b/src/app/_components/question.tsx @@ -3,8 +3,8 @@ import Link from 'next/link'; import { SubmitHandler, useForm } from 'react-hook-form'; import { RefObject, useEffect, useLayoutEffect, useRef } from 'react'; -import { CreateAnswerDto } from '@/app/_dto/create-answer/create-answer.dto'; -import { questionDto } from '@/app/_dto/question/question.dto'; +import { CreateAnswerDto } from '@/app/_dto/answers/create-answer.dto'; +import { questionDto } from '@/app/_dto/questions/question.dto'; interface formValue { answer: string; diff --git a/src/app/_dto/Answers.dto.ts b/src/app/_dto/answers/Answers.dto.ts similarity index 85% rename from src/app/_dto/Answers.dto.ts rename to src/app/_dto/answers/Answers.dto.ts index a5b0619..fc59139 100644 --- a/src/app/_dto/Answers.dto.ts +++ b/src/app/_dto/answers/Answers.dto.ts @@ -1,4 +1,4 @@ -import { userProfileDto } from './fetch-profile/Profile.dto'; +import { userProfileDto } from '../fetch-profile/Profile.dto'; export interface AnswerDto { id: string; diff --git a/src/app/_dto/create-answer/create-answer.dto.ts b/src/app/_dto/answers/create-answer.dto.ts similarity index 100% rename from src/app/_dto/create-answer/create-answer.dto.ts rename to src/app/_dto/answers/create-answer.dto.ts diff --git a/src/app/_dto/delete-answer/delete-answer.dto.ts b/src/app/_dto/answers/delete-answer.dto.ts similarity index 100% rename from src/app/_dto/delete-answer/delete-answer.dto.ts rename to src/app/_dto/answers/delete-answer.dto.ts diff --git a/src/app/_dto/fetch-all-answers/fetch-all-answers.dto.ts b/src/app/_dto/answers/fetch-all-answers.dto.ts similarity index 100% rename from src/app/_dto/fetch-all-answers/fetch-all-answers.dto.ts rename to src/app/_dto/answers/fetch-all-answers.dto.ts diff --git a/src/app/_dto/fetch-user-answers/fetch-user-answers.dto.ts b/src/app/_dto/answers/fetch-user-answers.dto.ts similarity index 100% rename from src/app/_dto/fetch-user-answers/fetch-user-answers.dto.ts rename to src/app/_dto/answers/fetch-user-answers.dto.ts diff --git a/src/app/_dto/create_question/create-question.dto.ts b/src/app/_dto/questions/create-question.dto.ts similarity index 100% rename from src/app/_dto/create_question/create-question.dto.ts rename to src/app/_dto/questions/create-question.dto.ts diff --git a/src/app/_dto/question/question.dto.ts b/src/app/_dto/questions/question.dto.ts similarity index 100% rename from src/app/_dto/question/question.dto.ts rename to src/app/_dto/questions/question.dto.ts diff --git a/src/app/_dto/websocket-event/websocket-event.dto.ts b/src/app/_dto/websocket-event/websocket-event.dto.ts index bdd3d97..c7fc061 100644 --- a/src/app/_dto/websocket-event/websocket-event.dto.ts +++ b/src/app/_dto/websocket-event/websocket-event.dto.ts @@ -1,5 +1,5 @@ -import { questionDto } from '@/app/_dto/question/question.dto'; -import { AnswerWithProfileDto } from '../Answers.dto'; +import { questionDto } from '@/app/_dto/questions/question.dto'; +import { AnswerWithProfileDto } from '../answers/Answers.dto'; export const event_name_enum_arr = [ 'question-created-event', @@ -43,4 +43,4 @@ export type AnswerDeletedEvPayload = { }; export class WebsocketAnswerDeletedEvent extends WebsocketEventPayload { ev_name: 'answer-deleted-event'; -} \ No newline at end of file +} diff --git a/src/app/api/_service/answer/answer-service.ts b/src/app/api/_service/answer/answer-service.ts index 9c9297e..ac12fd3 100644 --- a/src/app/api/_service/answer/answer-service.ts +++ b/src/app/api/_service/answer/answer-service.ts @@ -8,13 +8,13 @@ import { Auth, JwtPayload } from '@/api/_utils/jwt/decorator'; import { RateLimit } from '@/_service/ratelimiter/decorator'; import { userProfileDto } from '@/app/_dto/fetch-profile/Profile.dto'; import { $Enums, blocking, profile, user, server, PrismaClient } from '@prisma/client'; -import { AnswerListWithProfileDto, AnswerWithProfileDto } from '@/app/_dto/Answers.dto'; -import { FetchAllAnswersReqDto } from '@/app/_dto/fetch-all-answers/fetch-all-answers.dto'; -import { FetchUserAnswersDto } from '@/app/_dto/fetch-user-answers/fetch-user-answers.dto'; +import { AnswerListWithProfileDto, AnswerWithProfileDto } from '@/app/_dto/answers/Answers.dto'; +import { FetchAllAnswersReqDto } from '@/app/_dto/answers/fetch-all-answers.dto'; +import { FetchUserAnswersDto } from '@/app/_dto/answers/fetch-user-answers.dto'; import { RedisKvCacheService } from '@/app/api/_service/kvCache/redisKvCacheService'; import { RedisPubSubService } from '@/_service/redis-pubsub/redis-event.service'; import { AnswerDeletedEvPayload, QuestionDeletedPayload } from '@/app/_dto/websocket-event/websocket-event.dto'; -import { CreateAnswerDto } from '@/app/_dto/create-answer/create-answer.dto'; +import { CreateAnswerDto } from '@/app/_dto/answers/create-answer.dto'; import { profileToDto } from '@/api/_utils/profileToDto'; import { mastodonTootAnswers, MkNoteAnswers } from '@/app'; import { createHash } from 'crypto'; @@ -331,6 +331,38 @@ export class AnswerService { } } + @RateLimit({ bucket_time: 300, req_limit: 600 }, 'ip') + public async GetSingleAnswerApi(_req: NextRequest, answerId: string) { + const answer = await this.prisma.answer.findUnique({ + include: { answeredPerson: { include: { user: { include: { server: { select: { instanceType: true } } } } } } }, + where: { + id: answerId, + }, + }); + if (!answer) { + return sendApiError(404, 'Not found'); + } + const profileDto = profileToDto( + answer.answeredPerson, + answer.answeredPerson.user.hostName, + answer.answeredPerson.user.server.instanceType, + ); + const dto: AnswerWithProfileDto = { + id: answer.id, + question: answer.question, + questioner: answer.questioner, + answer: answer.answer, + answeredAt: answer.answeredAt, + answeredPerson: profileDto, + answeredPersonHandle: answer.answeredPersonHandle, + nsfwedAnswer: answer.nsfwedAnswer, + }; + return NextResponse.json(dto, { + status: 200, + headers: { 'Content-type': 'application/json', 'Cache-Control': 'public, max-age=60' }, + }); + } + private profileToDto(profile: profile, hostName: string, instanceType: $Enums.InstanceType): userProfileDto { const data: userProfileDto = { handle: profile.handle, diff --git a/src/app/api/_service/question/question-service.ts b/src/app/api/_service/question/question-service.ts index 0885ff4..18d6205 100644 --- a/src/app/api/_service/question/question-service.ts +++ b/src/app/api/_service/question/question-service.ts @@ -1,4 +1,4 @@ -import { CreateQuestionDto } from '@/app/_dto/create_question/create-question.dto'; +import { CreateQuestionDto } from '@/app/_dto/questions/create-question.dto'; import type { PrismaClient, user } from '@prisma/client'; import { NextRequest, NextResponse } from 'next/server'; import { validateStrict } from '@/utils/validator/strictValidator'; @@ -128,6 +128,7 @@ export class QuestionService { question: data.question, questioner: data.questioner, questioneeHandle: data.questionee, + isAnonymous: data.questioner ? false : true, //임시 }, }); diff --git a/src/app/api/_service/websocket/websocket-service.ts b/src/app/api/_service/websocket/websocket-service.ts index 82f9d8c..e9bc2b5 100644 --- a/src/app/api/_service/websocket/websocket-service.ts +++ b/src/app/api/_service/websocket/websocket-service.ts @@ -14,7 +14,7 @@ import { WebsocketEventPayload, WebsocketKeepAliveEvent, } from '@/app/_dto/websocket-event/websocket-event.dto'; -import { AnswerWithProfileDto } from '@/app/_dto/Answers.dto'; +import { AnswerWithProfileDto } from '@/app/_dto/answers/Answers.dto'; import { GetPrismaClient } from '../../_utils/getPrismaClient/get-prisma-client'; import { RedisKvCacheService } from '../kvCache/redisKvCacheService'; import { blocking, PrismaClient } from '@prisma/client'; diff --git a/src/app/api/db/answers/[userHandle]/[answerId]/route.ts b/src/app/api/db/answers/[userHandle]/[answerId]/route.ts index c4ea5b9..5b696ed 100644 --- a/src/app/api/db/answers/[userHandle]/[answerId]/route.ts +++ b/src/app/api/db/answers/[userHandle]/[answerId]/route.ts @@ -7,3 +7,9 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ a const answerId = (await params).answerId; return await service.deleteAnswer(req, answerId, null as unknown as jwtPayloadType); } + +export async function GET(req: NextRequest, { params }: { params: Promise<{ answerId: string }> }) { + const service = AnswerService.getInstance(); + const { answerId } = await params; + return await service.GetSingleAnswerApi(req, answerId); +} diff --git a/src/app/main/_events.ts b/src/app/main/_events.ts index ace8bd1..548fd28 100644 --- a/src/app/main/_events.ts +++ b/src/app/main/_events.ts @@ -1,7 +1,7 @@ import { Logger } from '@/utils/logger/Logger'; -import { questionDto } from '../_dto/question/question.dto'; +import { questionDto } from '../_dto/questions/question.dto'; import { QuestionDeletedPayload } from '../_dto/websocket-event/websocket-event.dto'; -import { AnswerWithProfileDto } from '../_dto/Answers.dto'; +import { AnswerWithProfileDto } from '../_dto/answers/Answers.dto'; const QuestionCreateEvent = 'QuestionCreateEvent'; const QuestionDeleteEvent = 'QuestionDeleteEvent'; diff --git a/src/app/main/layout.tsx b/src/app/main/layout.tsx index 882f41c..115897a 100644 --- a/src/app/main/layout.tsx +++ b/src/app/main/layout.tsx @@ -4,8 +4,8 @@ import { userProfileMeDto } from '@/app/_dto/fetch-profile/Profile.dto'; import MainHeader from '@/app/main/_header'; import { createContext, useEffect, useState } from 'react'; import { MyProfileContext } from '@/app/main/_profileContext'; -import { FetchAllAnswersReqDto } from '../_dto/fetch-all-answers/fetch-all-answers.dto'; -import { AnswerListWithProfileDto, AnswerWithProfileDto } from '../_dto/Answers.dto'; +import { FetchAllAnswersReqDto } from '../_dto/answers/fetch-all-answers.dto'; +import { AnswerListWithProfileDto, AnswerWithProfileDto } from '../_dto/answers/Answers.dto'; import { AnswerEv } from './_events'; type MainPageContextType = { diff --git a/src/app/main/questions/page.tsx b/src/app/main/questions/page.tsx index b7d36a5..6647d93 100644 --- a/src/app/main/questions/page.tsx +++ b/src/app/main/questions/page.tsx @@ -5,7 +5,7 @@ import { useContext, useEffect, useRef, useState } from 'react'; import DialogModalTwoButton from '@/app/_components/modalTwoButton'; import DialogModalLoadingOneButton from '@/app/_components/modalLoadingOneButton'; import { MyProfileContext } from '@/app/main/_profileContext'; -import { questionDto } from '@/app/_dto/question/question.dto'; +import { questionDto } from '@/app/_dto/questions/question.dto'; import { MyQuestionEv } from '../_events'; import { Logger } from '@/utils/logger/Logger'; import { QuestionDeletedPayload } from '@/app/_dto/websocket-event/websocket-event.dto'; diff --git a/src/app/main/user/[handle]/[answer]/action.ts b/src/app/main/user/[handle]/[answer]/action.ts deleted file mode 100644 index 7bdcf5a..0000000 --- a/src/app/main/user/[handle]/[answer]/action.ts +++ /dev/null @@ -1,17 +0,0 @@ -'use server'; - -import { GetPrismaClient } from '@/app/api/_utils/getPrismaClient/get-prisma-client'; - -export async function fetchAnswer(id: string) { - const prisma = GetPrismaClient.getClient(); - const answer = await prisma.answer.findUnique({ - include: { answeredPerson: true }, - where: { - id: id, - }, - }); - - if (answer) { - return answer; - } -} diff --git a/src/app/main/user/[handle]/[answer]/page.tsx b/src/app/main/user/[handle]/[answer]/page.tsx index 9f7bf4a..3879b4d 100644 --- a/src/app/main/user/[handle]/[answer]/page.tsx +++ b/src/app/main/user/[handle]/[answer]/page.tsx @@ -3,17 +3,27 @@ import Answer from '@/app/_components/answer'; import { useParams } from 'next/navigation'; import { useEffect, useRef, useState } from 'react'; -import { fetchAnswer } from './action'; -import { AnswerDto } from '@/app/_dto/Answers.dto'; +import { AnswerDto } from '@/app/_dto/answers/Answers.dto'; import DialogModalTwoButton from '@/app/_components/modalTwoButton'; export default function SingleAnswer() { const [answerBody, setAnswerBody] = useState(); const singleQuestionDeleteModalRef = useRef(null); const { answer } = useParams() as { answer: string }; + const { userHandle } = useParams() as { userHandle: string }; + + async function fetchAnswer(id: string) { + const res = await fetch(`/api/db/answers/${userHandle}/${id}`, { + method: 'GET', + }); + if (!res.ok) { + throw new Error(`Fail to fetch answer! ${await res.text()}`); + } + return await res.json(); + } const handleDeleteAnswer = async (id: string) => { - const res = await fetch(`/api/db/answers/${id}`, { + const res = await fetch(`/api/db/answers/${userHandle}/${id}`, { method: 'DELETE', }); try { diff --git a/src/app/main/user/[handle]/_answers.tsx b/src/app/main/user/[handle]/_answers.tsx index 9695305..883688d 100644 --- a/src/app/main/user/[handle]/_answers.tsx +++ b/src/app/main/user/[handle]/_answers.tsx @@ -4,8 +4,8 @@ import { useParams } from 'next/navigation'; import { useEffect, useRef, useState } from 'react'; import Answer from '@/app/_components/answer'; import { userProfileDto } from '@/app/_dto/fetch-profile/Profile.dto'; -import { AnswerDto } from '@/app/_dto/Answers.dto'; -import { FetchUserAnswersDto } from '@/app/_dto/fetch-user-answers/fetch-user-answers.dto'; +import { AnswerDto } from '@/app/_dto/answers/Answers.dto'; +import { FetchUserAnswersDto } from '@/app/_dto/answers/fetch-user-answers.dto'; import DialogModalTwoButton from '@/app/_components/modalTwoButton'; type ResponseType = { diff --git a/src/app/main/user/[handle]/_profile.tsx b/src/app/main/user/[handle]/_profile.tsx index 1c000be..5481a8b 100644 --- a/src/app/main/user/[handle]/_profile.tsx +++ b/src/app/main/user/[handle]/_profile.tsx @@ -4,7 +4,7 @@ import DialogModalLoadingOneButton from '@/app/_components/modalLoadingOneButton import DialogModalTwoButton from '@/app/_components/modalTwoButton'; import NameComponents from '@/app/_components/NameComponents'; import { SearchBlockListResDto } from '@/app/_dto/blocking/blocking.dto'; -import { CreateQuestionDto } from '@/app/_dto/create_question/create-question.dto'; +import { CreateQuestionDto } from '@/app/_dto/questions/create-question.dto'; import { userProfileDto } from '@/app/_dto/fetch-profile/Profile.dto'; import josa from '@/app/api/_utils/josa'; import Link from 'next/link';