diff --git a/src/app/_components/question.tsx b/src/app/_components/question.tsx index eef6c52..27c7039 100644 --- a/src/app/_components/question.tsx +++ b/src/app/_components/question.tsx @@ -223,7 +223,7 @@ export default function Question({ } async function postAnswer(req: CreateAnswerDto) { - const res = await fetch('/api/db/create-answer', { + const res = await fetch('/api/db/answers', { method: 'POST', body: JSON.stringify(req), headers: { 'Content-type': 'application/json' }, diff --git a/src/app/_dto/fetch-all-answers/fetch-all-answers.dto.ts b/src/app/_dto/fetch-all-answers/fetch-all-answers.dto.ts index 10c7b46..e07a247 100644 --- a/src/app/_dto/fetch-all-answers/fetch-all-answers.dto.ts +++ b/src/app/_dto/fetch-all-answers/fetch-all-answers.dto.ts @@ -1,3 +1,4 @@ +import { Transform } from 'class-transformer'; import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; export class FetchAllAnswersReqDto { @@ -15,6 +16,12 @@ export class FetchAllAnswersReqDto { @IsOptional() @IsInt() + @Transform((params) => { + if (typeof params.value === 'string') { + return parseInt(params.value, 10); + } + return params.value; + }) @Min(1) @Max(100) limit?: number; diff --git a/src/app/_dto/fetch-user-answers/fetch-user-answers.dto.ts b/src/app/_dto/fetch-user-answers/fetch-user-answers.dto.ts index 144e82c..994397d 100644 --- a/src/app/_dto/fetch-user-answers/fetch-user-answers.dto.ts +++ b/src/app/_dto/fetch-user-answers/fetch-user-answers.dto.ts @@ -1,4 +1,5 @@ -import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Max, Min } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; export class FetchUserAnswersDto { @IsOptional() @@ -17,9 +18,11 @@ export class FetchUserAnswersDto { @IsInt() @Min(1) @Max(100) + @Transform((param) => { + if (typeof param.value === 'string') { + return parseInt(param.value, 10); + } + return param.value; + }) limit?: number; - - @IsString() - @IsNotEmpty() - answeredPersonHandle: string; } diff --git a/src/app/api/_service/answer/answer-service.ts b/src/app/api/_service/answer/answer-service.ts index d4e34cc..9c9297e 100644 --- a/src/app/api/_service/answer/answer-service.ts +++ b/src/app/api/_service/answer/answer-service.ts @@ -1,4 +1,3 @@ -import { DeleteAnswerDto } from '@/app/_dto/delete-answer/delete-answer.dto'; import { sendApiError } from '@/api/_utils/apiErrorResponse/sendApiError'; import { validateStrict } from '@/utils/validator/strictValidator'; import { NextRequest, NextResponse } from 'next/server'; @@ -19,6 +18,8 @@ import { CreateAnswerDto } from '@/app/_dto/create-answer/create-answer.dto'; import { profileToDto } from '@/api/_utils/profileToDto'; import { mastodonTootAnswers, MkNoteAnswers } from '@/app'; import { createHash } from 'crypto'; +import { isString } from 'class-validator'; +import RE2 from 're2'; export class AnswerService { private static instance: AnswerService; @@ -150,17 +151,14 @@ export class AnswerService { @Auth() @RateLimit({ bucket_time: 600, req_limit: 300 }, 'user') - public async deleteAnswer(req: NextRequest, @JwtPayload tokenPayload?: jwtPayloadType) { - let data: DeleteAnswerDto; - try { - data = await validateStrict(DeleteAnswerDto, await req.json()); - } catch (err) { - return sendApiError(400, `Bad Request ${err}`); + public async deleteAnswer(req: NextRequest, answerId: string, @JwtPayload tokenPayload: jwtPayloadType) { + if (!isString(answerId)) { + return sendApiError(400, 'answerId is not string'); } const prisma = GetPrismaClient.getClient(); const willBeDeletedAnswer = await prisma.answer.findUnique({ - where: { id: data.id }, + where: { id: answerId }, }); if (!willBeDeletedAnswer) { // 그런 답변이 없음 @@ -171,9 +169,9 @@ export class AnswerService { return sendApiError(403, 'This is Not Your Answer!'); } try { - this.logger.log(`Delete answer... : ${data.id}`); - await prisma.answer.delete({ where: { id: data.id } }); - await this.event_service.pub('answer-deleted-event', { deleted_id: data.id }); + this.logger.log(`Delete answer... : ${answerId}`); + await prisma.answer.delete({ where: { id: answerId } }); + await this.event_service.pub('answer-deleted-event', { deleted_id: answerId }); return NextResponse.json({ message: 'Delete Answer Successful' }, { status: 200 }); } catch (err) { @@ -184,12 +182,18 @@ export class AnswerService { @Auth({ isOptional: true }) @RateLimit({ bucket_time: 600, req_limit: 600 }, 'ip') - public async fetchAll(req: NextRequest, @JwtPayload tokenPayload?: jwtPayloadType) { + public async GetAllAnswersApi(req: NextRequest, @JwtPayload tokenPayload?: jwtPayloadType) { const prisma = GetPrismaClient.getClient(); + const searchParams = req.nextUrl.searchParams; let data; try { - data = await validateStrict(FetchAllAnswersReqDto, await req.json()); + data = await validateStrict(FetchAllAnswersReqDto, { + untilId: searchParams.get('untilId') ?? undefined, + sinceId: searchParams.get('sinceId') ?? undefined, + sort: searchParams.get('sort') ?? undefined, + limit: searchParams.get('limit') ?? undefined, + }); } catch (err) { return sendApiError(400, `${err}`); } @@ -256,12 +260,19 @@ export class AnswerService { } @RateLimit({ bucket_time: 600, req_limit: 300 }, 'ip') - public async fetchUserAnswers(req: NextRequest) { + public async fetchUserAnswers(req: NextRequest, userHandle: string) { const prisma = GetPrismaClient.getClient(); + const searchParams = req.nextUrl.searchParams; + const query_params = { + limit: searchParams.get('limit') ?? undefined, + sinceId: searchParams.get('sinceId') ?? undefined, + untilId: searchParams.get('untilId') ?? undefined, + sort: searchParams.get('sort') ?? undefined, + }; try { let data; try { - data = await validateStrict(FetchUserAnswersDto, await req.json()); + data = await validateStrict(FetchUserAnswersDto, query_params); } catch (err) { return sendApiError(400, `${err}`); } @@ -273,12 +284,14 @@ export class AnswerService { //내림차순이 기본값 const orderBy = data.sort === 'ASC' ? 'asc' : 'desc'; - if (!data.answeredPersonHandle) { - throw new Error(`answeredPersonHandle is null`); + const re = new RE2(/^@.+@.+/); + if (!userHandle || !re.match(userHandle)) { + return sendApiError(400, 'User handle validation Error!'); } + const res = await prisma.answer.findMany({ where: { - answeredPersonHandle: data.answeredPersonHandle, + answeredPersonHandle: userHandle, id: { ...(typeof sinceId === 'string' ? { gt: sinceId } : {}), ...(typeof untilId === 'string' ? { lt: untilId } : {}), @@ -295,7 +308,7 @@ export class AnswerService { const answerCount = await prisma.profile.findMany({ where: { - handle: data.answeredPersonHandle, + handle: userHandle, }, select: { _count: { @@ -306,10 +319,13 @@ export class AnswerService { }, }); - return NextResponse.json({ - answers: res, - count: answerCount[0]._count.answer, - }); + return NextResponse.json( + { + answers: res, + count: answerCount[0]._count.answer, + }, + { headers: { 'Content-type': 'application/json', 'Cache-Control': 'private, no-store, max-age=0' } }, + ); } catch (err) { this.logger.log(err); } diff --git a/src/app/api/db/answers/[userHandle]/[answerId]/route.ts b/src/app/api/db/answers/[userHandle]/[answerId]/route.ts new file mode 100644 index 0000000..c4ea5b9 --- /dev/null +++ b/src/app/api/db/answers/[userHandle]/[answerId]/route.ts @@ -0,0 +1,9 @@ +import { AnswerService } from '@/app/api/_service/answer/answer-service'; +import { jwtPayloadType } from '@/app/api/_utils/jwt/jwtPayloadType'; +import { NextRequest } from 'next/server'; + +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ answerId: string }> }) { + const service = AnswerService.getInstance(); + const answerId = (await params).answerId; + return await service.deleteAnswer(req, answerId, null as unknown as jwtPayloadType); +} diff --git a/src/app/api/db/answers/[userHandle]/route.ts b/src/app/api/db/answers/[userHandle]/route.ts new file mode 100644 index 0000000..af5b3e8 --- /dev/null +++ b/src/app/api/db/answers/[userHandle]/route.ts @@ -0,0 +1,9 @@ +import { AnswerService } from '@/app/api/_service/answer/answer-service'; +import { NextRequest } from 'next/server'; + +export async function GET(req: NextRequest, { params }: { params: Promise<{ userHandle: string }> }) { + const answerService = AnswerService.getInstance(); + const { userHandle } = await params; + const handle = decodeURIComponent(userHandle); + return await answerService.fetchUserAnswers(req, handle); +} diff --git a/src/app/api/db/create-answer/route.ts b/src/app/api/db/answers/route.ts similarity index 71% rename from src/app/api/db/create-answer/route.ts rename to src/app/api/db/answers/route.ts index 6ac9e8c..2350297 100644 --- a/src/app/api/db/create-answer/route.ts +++ b/src/app/api/db/answers/route.ts @@ -6,3 +6,8 @@ export async function POST(req: NextRequest) { const service = AnswerService.getInstance(); return await service.createAnswerApi(req, null as unknown as jwtPayloadType); } + +export async function GET(req: NextRequest) { + const service = AnswerService.getInstance(); + return await service.GetAllAnswersApi(req); +} diff --git a/src/app/api/db/delete-answer/route.ts b/src/app/api/db/delete-answer/route.ts deleted file mode 100644 index 43e5345..0000000 --- a/src/app/api/db/delete-answer/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextRequest } from 'next/server'; -import { AnswerService } from '@/_service/answer/answer-service'; - -export async function POST(req: NextRequest) { - const service = AnswerService.getInstance(); - return await service.deleteAnswer(req); -} diff --git a/src/app/api/db/fetch-all-answers/route.ts b/src/app/api/db/fetch-all-answers/route.ts deleted file mode 100644 index 3f56091..0000000 --- a/src/app/api/db/fetch-all-answers/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AnswerService } from '@/_service/answer/answer-service'; -import { NextRequest } from 'next/server'; - -export async function POST(req: NextRequest) { - const service = AnswerService.getInstance(); - return await service.fetchAll(req); -} diff --git a/src/app/api/db/fetch-user-answers/route.ts b/src/app/api/db/fetch-user-answers/route.ts deleted file mode 100644 index a13b525..0000000 --- a/src/app/api/db/fetch-user-answers/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextRequest } from 'next/server'; -import { AnswerService } from '@/_service/answer/answer-service'; - -export async function POST(req: NextRequest) { - const service = AnswerService.getInstance(); - return await service.fetchUserAnswers(req); -} diff --git a/src/app/main/layout.tsx b/src/app/main/layout.tsx index 3255caa..882f41c 100644 --- a/src/app/main/layout.tsx +++ b/src/app/main/layout.tsx @@ -71,12 +71,14 @@ export default function MainLayout({ children }: { children: React.ReactNode }) } async function fetchAllAnswers(req: FetchAllAnswersReqDto) { - const res = await fetch('/api/db/fetch-all-answers', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(req), + const query: string[] = []; + for (const [key, value] of Object.entries(req)) { + query.push(`${key}=${value}`); + } + const url = `/api/db/answers?${query.join('&')}`; + const res = await fetch(url, { + method: 'GET', + cache: 'no-cache', }); try { if (res.ok) { diff --git a/src/app/main/user/[handle]/[answer]/page.tsx b/src/app/main/user/[handle]/[answer]/page.tsx index 8446070..9f7bf4a 100644 --- a/src/app/main/user/[handle]/[answer]/page.tsx +++ b/src/app/main/user/[handle]/[answer]/page.tsx @@ -13,9 +13,8 @@ export default function SingleAnswer() { const { answer } = useParams() as { answer: string }; const handleDeleteAnswer = async (id: string) => { - const res = await fetch('/api/db/delete-answer', { - method: 'POST', - body: JSON.stringify({ id: id }), + const res = await fetch(`/api/db/answers/${id}`, { + method: 'DELETE', }); try { if (res.ok) { diff --git a/src/app/main/user/[handle]/_answers.tsx b/src/app/main/user/[handle]/_answers.tsx index 8266729..9695305 100644 --- a/src/app/main/user/[handle]/_answers.tsx +++ b/src/app/main/user/[handle]/_answers.tsx @@ -41,9 +41,14 @@ export default function UserPage() { const answerDeleteModalRef = useRef(null); const fetchUserAnswers = async (q: FetchUserAnswersDto): Promise => { - const res = await fetch('/api/db/fetch-user-answers', { - method: 'POST', - body: JSON.stringify(q), + const params = Object.entries(q) + .map((e) => { + const [key, value] = e; + return `${key}=${encodeURIComponent(value)}`; + }) + .join('&'); + const res = await fetch(`/api/db/answers/${handle}?${params}`, { + method: 'GET', }); try { if (res.ok) { @@ -58,9 +63,8 @@ export default function UserPage() { }; const handleDeleteAnswer = async (id: string) => { - const res = await fetch('/api/db/delete-answer', { - method: 'POST', - body: JSON.stringify({ id: id }), + const res = await fetch(`/api/db/answers/${handle}/${encodeURIComponent(id)}`, { + method: 'DELETE', }); if (!res.ok) { alert(`답변을 삭제하는데 실패하였습니다! ${await res.text()}`); @@ -82,7 +86,6 @@ export default function UserPage() { useEffect(() => { if (userProfile) { fetchUserAnswers({ - answeredPersonHandle: userProfile.handle, sort: 'DESC', limit: 20, }).then(({ answers, count }: ResponseType) => { @@ -106,7 +109,6 @@ export default function UserPage() { sort: 'DESC', limit: 20, untilId: untilId, - answeredPersonHandle: profileHandle, }).then((r) => { if (r.answers.length === 0) { setLoading(false);