Skip to content

Commit

Permalink
Merge pull request #108 from serafuku/yunochi/develop
Browse files Browse the repository at this point in the history
🎨 answer API구조 refactor
  • Loading branch information
yunochi authored Dec 15, 2024
2 parents 229bab2 + a62d183 commit 99c6536
Show file tree
Hide file tree
Showing 13 changed files with 98 additions and 67 deletions.
2 changes: 1 addition & 1 deletion src/app/_components/question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
7 changes: 7 additions & 0 deletions src/app/_dto/fetch-all-answers/fetch-all-answers.dto.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Transform } from 'class-transformer';
import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';

export class FetchAllAnswersReqDto {
Expand All @@ -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;
Expand Down
13 changes: 8 additions & 5 deletions src/app/_dto/fetch-user-answers/fetch-user-answers.dto.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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;
}
62 changes: 39 additions & 23 deletions src/app/api/_service/answer/answer-service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
// 그런 답변이 없음
Expand All @@ -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<AnswerDeletedEvPayload>('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<AnswerDeletedEvPayload>('answer-deleted-event', { deleted_id: answerId });

return NextResponse.json({ message: 'Delete Answer Successful' }, { status: 200 });
} catch (err) {
Expand All @@ -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}`);
}
Expand Down Expand Up @@ -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}`);
}
Expand All @@ -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 } : {}),
Expand All @@ -295,7 +308,7 @@ export class AnswerService {

const answerCount = await prisma.profile.findMany({
where: {
handle: data.answeredPersonHandle,
handle: userHandle,
},
select: {
_count: {
Expand All @@ -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);
}
Expand Down
9 changes: 9 additions & 0 deletions src/app/api/db/answers/[userHandle]/[answerId]/route.ts
Original file line number Diff line number Diff line change
@@ -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);
}
9 changes: 9 additions & 0 deletions src/app/api/db/answers/[userHandle]/route.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
7 changes: 0 additions & 7 deletions src/app/api/db/delete-answer/route.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/app/api/db/fetch-all-answers/route.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/app/api/db/fetch-user-answers/route.ts

This file was deleted.

14 changes: 8 additions & 6 deletions src/app/main/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 2 additions & 3 deletions src/app/main/user/[handle]/[answer]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
18 changes: 10 additions & 8 deletions src/app/main/user/[handle]/_answers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@ export default function UserPage() {
const answerDeleteModalRef = useRef<HTMLDialogElement>(null);

const fetchUserAnswers = async (q: FetchUserAnswersDto): Promise<ResponseType> => {
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) {
Expand All @@ -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()}`);
Expand All @@ -82,7 +86,6 @@ export default function UserPage() {
useEffect(() => {
if (userProfile) {
fetchUserAnswers({
answeredPersonHandle: userProfile.handle,
sort: 'DESC',
limit: 20,
}).then(({ answers, count }: ResponseType) => {
Expand All @@ -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);
Expand Down

0 comments on commit 99c6536

Please sign in to comment.