diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 612b33f..b3090a0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -99,16 +99,16 @@ model server { model blocking { id String @id @default(cuid()) - blockeeHandle String @db.VarChar(500) + blockeeTarget String @db.VarChar(500) @map("blockeeHandle") /// Handle or ipHash blockerHandle String @db.VarChar(500) blocker user @relation("blocks", fields: [blockerHandle], references: [handle], onDelete: Cascade) createdAt DateTime @default(now()) hidden Boolean @default(false) imported Boolean @default(false) - @@unique([blockeeHandle, blockerHandle, hidden, imported]) + @@unique([blockeeTarget, blockerHandle, hidden, imported]) @@index([blockerHandle, hidden]) - @@index([blockeeHandle, hidden]) + @@index([blockeeTarget, hidden]) @@index([imported]) } diff --git a/src/app/_components/question.tsx b/src/app/_components/question.tsx index fb2303a..e79da45 100644 --- a/src/app/_components/question.tsx +++ b/src/app/_components/question.tsx @@ -20,6 +20,7 @@ interface askProps { setId: React.Dispatch>; deleteRef: RefObject; answerRef: RefObject; + blockingRef: RefObject; setIsLoading: React.Dispatch>; defaultVisibility: 'public' | 'home' | 'followers' | undefined; } @@ -31,6 +32,7 @@ export default function Question({ setId, deleteRef, answerRef, + blockingRef, setIsLoading, defaultVisibility, }: askProps) { @@ -172,6 +174,15 @@ export default function Question({ > 삭제 + { + setId(singleQuestion.id); + blockingRef.current?.showModal(); + }} + > + 질문자 차단 +
diff --git a/src/app/_dto/blocking/blocking.dto.ts b/src/app/_dto/blocking/blocking.dto.ts index ac71c9f..707c8e5 100644 --- a/src/app/_dto/blocking/blocking.dto.ts +++ b/src/app/_dto/blocking/blocking.dto.ts @@ -1,3 +1,4 @@ +import { blocking, question } from '@prisma/client'; import { IsBoolean, IsEnum, IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator'; export class CreateBlockDto { @@ -5,7 +6,10 @@ export class CreateBlockDto { @MaxLength(500) targetHandle: string; } - +export class createBlockByQuestionDto { + @IsInt() + questionId: question['id']; +} export class Block { id: string; targetHandle: string; @@ -53,3 +57,9 @@ export class DeleteBlockDto { @MaxLength(500) targetHandle: string; } + +export class DeleteBlockByIdDto { + @IsString() + @MaxLength(200) + targetId: blocking['id']; +} diff --git a/src/app/api/_service/answer/answer-service.ts b/src/app/api/_service/answer/answer-service.ts index 5b5b815..8e4ccf4 100644 --- a/src/app/api/_service/answer/answer-service.ts +++ b/src/app/api/_service/answer/answer-service.ts @@ -388,7 +388,7 @@ export class AnswerService { const existList = []; for (const block of all_blockList) { const exist = await prisma.user.findUnique({ - where: { handle: block.blockeeHandle }, + where: { handle: block.blockeeTarget }, }); if (exist) { existList.push(block); @@ -397,9 +397,9 @@ export class AnswerService { return existList; }; const blockList = await kv.get(getBlockListOnlyExist, { key: `block-${myHandle}`, ttl: 600 }); - const blockedList = await prisma.blocking.findMany({ where: { blockeeHandle: myHandle, hidden: false } }); + const blockedList = await prisma.blocking.findMany({ where: { blockeeTarget: myHandle, hidden: false } }); const filteredAnswers = answers.filter((ans) => { - if (blockList.find((b) => b.blockeeHandle === ans.answeredPersonHandle || b.blockeeHandle === ans.questioner)) { + if (blockList.find((b) => b.blockeeTarget === ans.answeredPersonHandle || b.blockeeTarget === ans.questioner)) { return false; } if (blockedList.find((b) => b.blockerHandle === ans.answeredPersonHandle || b.blockerHandle === ans.questioner)) { diff --git a/src/app/api/_service/blocking/blocking-service.ts b/src/app/api/_service/blocking/blocking-service.ts index 8ab161a..3958e72 100644 --- a/src/app/api/_service/blocking/blocking-service.ts +++ b/src/app/api/_service/blocking/blocking-service.ts @@ -7,7 +7,9 @@ import { GetPrismaClient } from '@/api/_utils/getPrismaClient/get-prisma-client' import { validateStrict } from '@/utils/validator/strictValidator'; import { Block, + createBlockByQuestionDto, CreateBlockDto, + DeleteBlockByIdDto, DeleteBlockDto, GetBlockListReqDto, SearchBlockListReqDto, @@ -56,7 +58,8 @@ export class BlockingService { return sendApiError(400, 'Bad Request. User not found'); } try { - await this.createBlock(targetUser.handle, user.handle, false); + const b = await this.createBlock(targetUser.handle, user.handle, false); + this.logger.debug(`New Block created, hidden: ${b.hidden}, target: ${b.blockeeTarget}`); } catch (err) { return sendApiError(500, JSON.stringify(err)); } @@ -64,6 +67,38 @@ export class BlockingService { return NextResponse.json({}, { status: 200 }); } + @Auth() + @RateLimit({ bucket_time: 300, req_limit: 60 }, 'user') + public async createBlockByQuestionApi( + req: NextRequest, + @JwtPayload tokenBody?: jwtPayloadType, + ): Promise { + let data; + try { + data = await validateStrict(createBlockByQuestionDto, await req.json()); + } catch (err) { + return sendApiError(400, `Bad request ${String(err)}`); + } + try { + const q = await this.prisma.question.findUnique({ where: { id: data.questionId } }); + if (!q) { + return sendApiError(400, 'questionId not found!'); + } + if (q.questioneeHandle !== tokenBody?.handle) { + return sendApiError(403, 'Not your question!'); + } + if (q.questioner) { + const b = await this.createBlock(q.questioner, tokenBody.handle, false, q.isAnonymous); + this.logger.debug(`New Block created by Question ${q.id}, hidden: ${b.hidden}, target: ${b.blockeeTarget}`); + return NextResponse.json(`OK. block created!`, { status: 201 }); + } else { + return NextResponse.json(`Block not created! (questioner is null)`, { status: 200 }); + } + } catch (err) { + return sendApiError(500, 'ERROR!' + String(err)); + } + } + @Auth() @RateLimit({ bucket_time: 300, req_limit: 150 }, 'user') public async getBlockList(req: NextRequest, @JwtPayload tokenBody?: jwtPayloadType) { @@ -86,7 +121,6 @@ export class BlockingService { ...(data.sinceId ? { gt: data.sinceId } : {}), ...(data.untilId ? { lt: data.untilId } : {}), }, - hidden: false, }, orderBy: { id: orderBy }, take: data.limit ?? 10, @@ -95,7 +129,7 @@ export class BlockingService { const return_data = blockList.map((v) => { const d: Block = { id: v.id, - targetHandle: v.blockeeHandle, + targetHandle: v.hidden ? `익명의 질문자 ${v.id}` : v.blockeeTarget, blockedAt: v.createdAt, }; return d; @@ -125,7 +159,7 @@ export class BlockingService { const r = await this.prisma.blocking.findMany({ where: { blockerHandle: tokenBody.handle, - blockeeHandle: data.targetHandle, + blockeeTarget: data.targetHandle, hidden: false, }, }); @@ -139,23 +173,44 @@ export class BlockingService { public async deleteBlock(req: NextRequest, @JwtPayload tokenBody?: jwtPayloadType) { let data; try { - data = await validateStrict(DeleteBlockDto, await req.json()); - } catch { - return sendApiError(400, 'Bad Request'); + const reqJson = await req.json(); + if (reqJson.targetId) { + data = await validateStrict(DeleteBlockByIdDto, reqJson); + } else { + data = await validateStrict(DeleteBlockDto, reqJson); + } + } catch (err) { + return sendApiError(400, `Bad Request ${String(err)}`); } + const user = await this.prisma.user.findUniqueOrThrow({ where: { handle: tokenBody!.handle } }); try { - const r = await this.prisma.blocking.deleteMany({ - where: { - blockeeHandle: data.targetHandle, - blockerHandle: user.handle, - hidden: false, - }, - }); - this.logger.debug(`${r.count} block deleted`); + const deleteById = (data as DeleteBlockByIdDto).targetId ? (data as DeleteBlockByIdDto) : null; + const deleteByHandle = (data as DeleteBlockDto).targetHandle ? (data as DeleteBlockDto) : null; + if (deleteById) { + const r = await this.prisma.blocking.deleteMany({ + where: { + id: deleteById.targetId, + blockerHandle: user.handle, + }, + }); + this.logger.debug(`${r.count} block deleted (by id ${deleteById.targetId})`); + return NextResponse.json({ message: `${r.count} block deleted (by id ${deleteById.targetId})` }); + } + if (deleteByHandle) { + const r = await this.prisma.blocking.deleteMany({ + where: { + blockeeTarget: deleteByHandle.targetHandle, + blockerHandle: user.handle, + hidden: false, + }, + }); + this.logger.debug(`${r.count} block deleted`); + return NextResponse.json({ message: `${r.count} block deleted` }); + } } catch { - return sendApiError(400, '이미 차단 해제된 사용자입니다!'); + return sendApiError(500, '차단 해제 오류'); } await this.redisKvService.drop(`block-${user.handle}`); @@ -181,23 +236,23 @@ export class BlockingService { /** * 블락을 생성하고, 미답변 질문중 블락대상의 것은 삭제 - * @param blockeeHandle 블락될 대상의 핸들 + * @param blockeeTarget 블락될 대상의 핸들이나 ipHash * @param myHandle 내 핸들 * @param imported ImportBlock에 의해서 가져온 Block인 경우 true - * @param isHidden 익명질문의 유저를 차단하는 경우 true (구현 예정) + * @param isHidden 익명질문의 유저를 차단하는 경우 true */ - public async createBlock(blockeeHandle: string, myHandle: string, imported?: boolean, isHidden?: boolean) { + public async createBlock(blockeeTarget: string, myHandle: string, imported?: boolean, isHidden?: boolean) { const dbData = { - blockeeHandle: blockeeHandle, + blockeeTarget: blockeeTarget, blockerHandle: myHandle, hidden: isHidden ? true : false, imported: imported ? true : false, createdAt: new Date(Date.now()), }; - await this.prisma.blocking.upsert({ + const b = await this.prisma.blocking.upsert({ where: { - blockeeHandle_blockerHandle_hidden_imported: { - blockeeHandle: dbData.blockeeHandle, + blockeeTarget_blockerHandle_hidden_imported: { + blockeeTarget: dbData.blockeeTarget, blockerHandle: dbData.blockerHandle, hidden: dbData.hidden, imported: dbData.imported, @@ -209,7 +264,7 @@ export class BlockingService { //기존 질문의 필터링 const question_list = await this.prisma.question.findMany({ where: { questioneeHandle: myHandle } }); - const remove_list = question_list.filter((q) => q.questioner === blockeeHandle); + const remove_list = question_list.filter((q) => q.questioner === blockeeTarget); remove_list.forEach(async (r) => { await this.prisma.question.delete({ where: { id: r.id } }); const ev_data: QuestionDeletedPayload = { @@ -220,5 +275,7 @@ export class BlockingService { await this.redisPubsubService.pub('question-deleted-event', ev_data); }); await this.redisKvService.drop(`block-${myHandle}`); + + return b; } } diff --git a/src/app/api/_service/question/question-service.ts b/src/app/api/_service/question/question-service.ts index 51e76f0..4e38594 100644 --- a/src/app/api/_service/question/question-service.ts +++ b/src/app/api/_service/question/question-service.ts @@ -81,13 +81,13 @@ export class QuestionService { return sendApiError(403, 'User stops NewQuestion'); } // 블락 여부 검사 - if (tokenPayload?.handle) { - const blocked = await this.prisma.blocking.findFirst({ - where: { blockeeHandle: tokenPayload.handle, blockerHandle: questionee_user.handle }, - }); - if (blocked) { - return sendApiError(403, '이 사용자에게 질문을 보낼 수 없습니다!'); - } + + const blockeeTarget = tokenPayload?.handle ?? getIpHash(getIpFromRequest(req)); + const blocked = await this.prisma.blocking.findFirst({ + where: { blockeeTarget: blockeeTarget, blockerHandle: questionee_user.handle }, + }); + if (blocked) { + return sendApiError(403, '이 사용자에게 질문을 보낼 수 없습니다!'); } if (!data.isAnonymous && !tokenPayload?.handle) { diff --git a/src/app/api/_service/websocket/websocket-service.ts b/src/app/api/_service/websocket/websocket-service.ts index ccb7f36..bfd9c1a 100644 --- a/src/app/api/_service/websocket/websocket-service.ts +++ b/src/app/api/_service/websocket/websocket-service.ts @@ -275,7 +275,7 @@ export class WebsocketService { const existList = []; for (const block of all_blockList) { const exist = await this.prisma.user.findUnique({ - where: { handle: block.blockeeHandle }, + where: { handle: block.blockeeTarget }, }); if (exist) { existList.push(block); @@ -287,9 +287,9 @@ export class WebsocketService { key: `block-${target_handle}`, ttl: 600, }); - const blockedList = await this.prisma.blocking.findMany({ where: { blockeeHandle: target_handle, hidden: false } }); + const blockedList = await this.prisma.blocking.findMany({ where: { blockeeTarget: target_handle, hidden: false } }); const filteredClients = client_list.filter((c) => { - if (blockList.find((b) => b.blockeeHandle === c.user_handle)) { + if (blockList.find((b) => b.blockeeTarget === c.user_handle)) { return false; } if (blockedList.find((b) => b.blockerHandle === c.user_handle)) { diff --git a/src/app/api/user/blocking/create-by-question/route.ts b/src/app/api/user/blocking/create-by-question/route.ts new file mode 100644 index 0000000..65208cc --- /dev/null +++ b/src/app/api/user/blocking/create-by-question/route.ts @@ -0,0 +1,7 @@ +import { NextRequest } from 'next/server'; +import { BlockingService } from '@/app/api/_service/blocking/blocking-service'; + +export async function POST(req: NextRequest) { + const service = BlockingService.get(); + return await service.createBlockByQuestionApi(req); +} diff --git a/src/app/main/questions/page.tsx b/src/app/main/questions/page.tsx index 54a6a59..9c46594 100644 --- a/src/app/main/questions/page.tsx +++ b/src/app/main/questions/page.tsx @@ -9,6 +9,7 @@ import { MyQuestionEv } from '../_events'; import { Logger } from '@/utils/logger/Logger'; import { QuestionDeletedPayload } from '@/app/_dto/websocket-event/websocket-event.dto'; import { MyProfileContext } from '@/app/main/layout'; +import { createBlockByQuestionDto } from '@/app/_dto/blocking/blocking.dto'; const fetchQuestions = async (): Promise => { const res = await fetch('/api/db/questions'); @@ -36,6 +37,19 @@ async function deleteQuestion(id: number) { throw new Error(`질문을 삭제하는데 실패했어요! ${await res.text()}`); } } +async function createBlock(id: number) { + const body: createBlockByQuestionDto = { + questionId: id, + }; + const res = await fetch(`/api/user/blocking/create-by-question`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!res.ok) { + throw new Error(`차단에 실패했어요! ${await res.text()}`); + } +} export default function Questions() { const [questions, setQuestions] = useState(); @@ -43,6 +57,7 @@ export default function Questions() { const [id, setId] = useState(0); const deleteQuestionModalRef = useRef(null); const answeredQuestionModalRef = useRef(null); + const createBlockModalRef = useRef(null); const [isLoading, setIsLoading] = useState(false); const onNewQuestionEvent = (ev: CustomEvent) => { @@ -92,6 +107,7 @@ export default function Questions() { setQuestions={setQuestions} answerRef={answeredQuestionModalRef} deleteRef={deleteQuestionModalRef} + blockingRef={createBlockModalRef} setIsLoading={setIsLoading} defaultVisibility={profile?.defaultPostVisibility} /> @@ -132,6 +148,16 @@ export default function Questions() { setQuestions((prevQuestions) => (prevQuestions ? [...prevQuestions.filter((prev) => prev.id !== id)] : null)); }} /> + { + createBlock(id); + }} + />
); } diff --git a/src/app/main/settings/_table.tsx b/src/app/main/settings/_table.tsx index 0754302..a009f51 100644 --- a/src/app/main/settings/_table.tsx +++ b/src/app/main/settings/_table.tsx @@ -3,13 +3,13 @@ import CollapseMenu from '@/app/_components/collapseMenu'; import DialogModalLoadingOneButton from '@/app/_components/modalLoadingOneButton'; import DialogModalTwoButton from '@/app/_components/modalTwoButton'; -import { Block, GetBlockListReqDto, GetBlockListResDto } from '@/app/_dto/blocking/blocking.dto'; +import { Block, DeleteBlockByIdDto, GetBlockListReqDto, GetBlockListResDto } from '@/app/_dto/blocking/blocking.dto'; import { useEffect, useRef, useState } from 'react'; export default function BlockList() { const [untilId, setUntilId] = useState(null); const [blockList, setBlockList] = useState([]); - const [unblockHandle, setUnblockHandle] = useState(null); + const [unblockId, setUnblockId] = useState(null); const [isLoading, setIsLoading] = useState(true); const [mounted, setMounted] = useState(null); const [loadingDoneModalText, setLoadingDoneModalText] = useState<{ title: string; body: string }>({ @@ -19,12 +19,13 @@ export default function BlockList() { const unblockConfirmModalRef = useRef(null); const unblockSuccessModalRef = useRef(null); - const handleUnBlock = async (handle: string) => { + const doUnBlock = async (id: string) => { setIsLoading(true); unblockSuccessModalRef.current?.showModal(); + const data: DeleteBlockByIdDto = { targetId: id }; const res = await fetch('/api/user/blocking/delete', { method: 'POST', - body: JSON.stringify({ targetHandle: handle }), + body: JSON.stringify(data), }); if (!res.ok) { setIsLoading(false); @@ -34,7 +35,7 @@ export default function BlockList() { }); return; } - setBlockList((prevList) => (prevList ? [...prevList.filter((prev) => prev.targetHandle !== handle)] : [])); + setBlockList((prevList) => (prevList ? [...prevList.filter((prev) => prev.id !== id)] : [])); setIsLoading(false); }; @@ -95,7 +96,7 @@ export default function BlockList() {