diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9238a53..c50b990 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -90,8 +90,8 @@ model UserAgreement { termsAgreed Boolean @map("terms_agreed") privacyAgreed Boolean @map("privacy_agreed") ageOver14Agreed Boolean @map("age_over_14_agreed") - marketingPushAgreed Boolean @map("marketing_push_agreed") - marketingEmailAgreed Boolean @map("marketing_email_agreed") + marketingPushAgreed Boolean @default(false) @map("marketing_push_agreed") + marketingEmailAgreed Boolean @default(false) @map("marketing_email_agreed") agreedAt DateTime @default(now()) @map("agreed_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) diff --git a/src/controllers/mailbox.controller.js b/src/controllers/mailbox.controller.js index 048e260..635059a 100644 --- a/src/controllers/mailbox.controller.js +++ b/src/controllers/mailbox.controller.js @@ -21,8 +21,8 @@ export const handleGetAnonymousThreadLetters = async (req, res, next) => { const userId = getAuthUserId(req); if (!userId) throw new MailboxUnauthorizedError(); - const { threadId } = req.params; - const result = await getAnonymousThreadLetters(userId, threadId); + const { sessionId } = req.params; + const result = await getAnonymousThreadLetters(userId, sessionId); return res.status(200).success(result); } catch (err) { diff --git a/src/errors/mailbox.error.js b/src/errors/mailbox.error.js index e54b0cb..deae07e 100644 --- a/src/errors/mailbox.error.js +++ b/src/errors/mailbox.error.js @@ -7,8 +7,8 @@ export class MailboxUnauthorizedError extends unauthorizedError { } } -export class MailboxInvalidThreadIdError extends BadRequestError { - constructor(code = "MAILBOX_INVALID_THREAD_ID", message = "threadId가 올바르지 않습니다.", data = null) { +export class MailboxInvalidSessionIdError extends BadRequestError { + constructor(code = "MAILBOX_INVALID_SESSION_ID", message = "sessionId가 올바르지 않습니다.", data = null) { super(code, message, data); } } diff --git a/src/index.js b/src/index.js index c99f26b..75d8af4 100644 --- a/src/index.js +++ b/src/index.js @@ -27,7 +27,7 @@ import { isRestricted } from "./middlewares/restriction.middleware.js"; import { letterByAiKeywordSchema, letterToMeSchema, letterToOtherSchema, publicCarouselSchema } from "./schemas/letter.schema.js"; import { idParamSchema, ISOTimeSchema } from "./schemas/common.schema.js"; import { pushSubscriptionSchema, onboardingStep1Schema, updateInterestsSchema, updateProfileSchema, updateNotificationSettingsSchema, updateConsentsSchema, updateActivitySchema, createUserAgreementsSchema } from "./schemas/user.schema.js"; -import { threadIdParamSchema } from "./schemas/mailbox.schema.js"; +import { sessionIdParamSchema } from "./schemas/mailbox.schema.js"; import { noticeIdParamSchema } from "./schemas/notice.schema.js"; import { HandleGetHomeDashboard } from "./controllers/dashboard.controller.js"; import { handleInsertUserReport, handleGetUserReports, handleGetUserReport } from "./controllers/report.controller.js"; @@ -210,7 +210,7 @@ app.put("/users/me/push-subscriptions", isLogin, validate(pushSubscriptionSchema // / 편지함 app.get("/mailbox/anonymous", isLogin, handleGetAnonymousThreads); -app.get("/mailbox/anonymous/threads/:threadId/letters", isLogin, validate(threadIdParamSchema), handleGetAnonymousThreadLetters); +app.get("/mailbox/anonymous/threads/:sessionId/letters", isLogin, validate(sessionIdParamSchema), handleGetAnonymousThreadLetters); app.get("/mailbox/friends/threads/:friendId/letters", isLogin, validate(idParamSchema("friendId")), handleGetLetterFromFriend); // 친구 대화 목록 화면 조회 app.get("/mailbox/self", isLogin, handleGetSelfMailbox); diff --git a/src/jobs/bootstraps/push.bootstrap.js b/src/jobs/bootstraps/push.bootstrap.js index 5b60dd7..1366441 100644 --- a/src/jobs/bootstraps/push.bootstrap.js +++ b/src/jobs/bootstraps/push.bootstrap.js @@ -1,6 +1,8 @@ import { Queue, Worker } from "bullmq"; import { ioredisConnection } from "../../configs/db.config.js"; import { sendPushNotification } from "../../services/push.service.js"; +import { findRecentNotices, findUsersWithMarketingPushEnabled } from "../../repositories/user.repository.js"; +import { enqueueJob } from "../../utils/queue.util.js"; export const pushQueue = new Queue("send-push", { connection: ioredisConnection }); @@ -12,6 +14,18 @@ export const sendPushNotificationWorker = () => { console.log(`[Job Success] 푸시 알람 전송에 성공했습니다.`); } + + if(job.name === "PUSH_BY_NOTICE") { + console.log(`[Job Start] 공지사항 푸시알림 전송 중. userId: ${job.data.userId}`); + await sendPushNotification({ + userId: job.data.userId, + type: "NOTICE", + data: {}, + useMarketing: true, + }); + + console.log(`[Job Success] 공지사항 푸시 알림 전송에 성공했습니다. userId: ${job.data.userId}`); + } }, {connection: ioredisConnection}); worker.on('failed', (job, err) => { @@ -21,4 +35,48 @@ export const sendPushNotificationWorker = () => { worker.on('error', (err) => { console.error(`[Worker Error] 시스템 오류: ${err.message}`); }); -} \ No newline at end of file +}; + +/** + * 최근 공지사항을 찾아서 큐에 푸시 알림 job 추가 + */ +export const sendNoticePushNotifications = async () => { + // 10분 이내 생성된 공지사항 조회 (기존 함수 재사용) + const recentNotices = await findRecentNotices(10); + + if (recentNotices.length === 0) { + console.log("[Cron Info] 최근 10분 이내 생성된 공지사항이 없습니다."); + return { queued: 0, notices: [] }; + } + + console.log(`[Cron Info] ${recentNotices.length}개의 최근 공지사항을 찾았습니다.`); + + // marketingEnabled: true인 사용자 조회 (기존 함수 재사용) + const userIds = await findUsersWithMarketingPushEnabled(); + + if (userIds.length === 0) { + console.log("[Cron Info] 푸시 알림이 활성화된 사용자가 없습니다."); + return { queued: 0, notices: recentNotices, users: 0 }; + } + + console.log(`[Cron Info] ${userIds.length}명의 사용자에게 푸시 알림을 큐에 추가합니다.`); + + // 각 사용자에게 푸시 알림 job 추가 + const jobs = await Promise.allSettled( + userIds.map((userId) => + enqueueJob(pushQueue, "PUSH_BY_NOTICE", { userId }) + ) + ); + + const successCount = jobs.filter((r) => r.status === "fulfilled").length; + const failCount = jobs.filter((r) => r.status === "rejected").length; + + console.log(`[Cron Success] 푸시 알림 job 추가 완료: 성공 ${successCount}건, 실패 ${failCount}건`); + + return { + queued: successCount, + failed: failCount, + notices: recentNotices, + users: userIds.length, + }; +}; \ No newline at end of file diff --git a/src/jobs/crons/notice.cron.js b/src/jobs/crons/notice.cron.js new file mode 100644 index 0000000..cc007cf --- /dev/null +++ b/src/jobs/crons/notice.cron.js @@ -0,0 +1,17 @@ +import cron from "node-cron"; +import { sendNoticePushNotifications } from "../bootstraps/push.bootstrap.js"; + +export const sendNoticePushCron = () => { + // 10분마다 실행 + cron.schedule( + '*/10 * * * *', + async () => { + console.log("[Cron Start] 공지사항 푸시 알림 전송을 시작합니다."); + await sendNoticePushNotifications(); + }, + { + scheduled: true, + timezone: "Asia/Seoul" + } + ); +}; diff --git a/src/jobs/index.job.js b/src/jobs/index.job.js index 548daa5..fbd12df 100644 --- a/src/jobs/index.job.js +++ b/src/jobs/index.job.js @@ -1,5 +1,6 @@ import { sendScheduledLettersCron } from "./crons/letter.cron.js"; -import { startWeeklyReportCron } from "./crons/weeklyReport.cron.js" +import { startWeeklyReportCron } from "./crons/weeklyReport.cron.js"; +import { sendNoticePushCron } from "./crons/notice.cron.js"; import { sendQueuedLettersWorker } from "./bootstraps/letter.bootstrap.js" import { sendPushNotificationWorker } from "./bootstraps/push.bootstrap.js"; import { sendMailWorker } from "./bootstraps/mail.bootstrap.js"; @@ -7,6 +8,7 @@ import { sendMailWorker } from "./bootstraps/mail.bootstrap.js"; export const startBatch = async () => { startWeeklyReportCron(); sendScheduledLettersCron(); + sendNoticePushCron(); sendQueuedLettersWorker(); sendPushNotificationWorker(); diff --git a/src/repositories/letter.repository.js b/src/repositories/letter.repository.js index 292a3a3..c695160 100644 --- a/src/repositories/letter.repository.js +++ b/src/repositories/letter.repository.js @@ -1,5 +1,6 @@ import { prisma } from "../configs/db.config.js" import { ReferenceNotFoundError } from "../errors/base.error.js"; +import { LETTER_TYPE_ANON } from "../utils/user.util.js"; export const getLetterByUserIdAndAiKeyword = async (senderUserId, keyword) => { const letter = await prisma.letter.findMany({ @@ -349,6 +350,23 @@ export const selectLetterByUserIds = async (userId, targetUserId) => { return count; }; +export const selectAnonymousLetterCountByUserIds = async (userId, targetUserId) => { + if (!userId || !targetUserId) { + return 0; + } + + const count = await prisma.letter.count({ + where: { + letterType: LETTER_TYPE_ANON, + OR: [ + { senderUserId: userId, receiverUserId: targetUserId }, + { senderUserId: targetUserId, receiverUserId: userId }, + ], + }, + }); + return count; +}; + export const selectRecentLetterByUserIds = async (userId, targetUserId) => { return await prisma.letter.findFirst({ where: { diff --git a/src/repositories/user.repository.js b/src/repositories/user.repository.js index b1ce0f8..31bec79 100644 --- a/src/repositories/user.repository.js +++ b/src/repositories/user.repository.js @@ -325,6 +325,21 @@ export const getPushSubscription = async (userId) => { return subscriptions; } +export const getPushSubscriptionForMarketing = async (userId) => { + const subscriptions = await prisma.pushSubscription.findMany({ + where: { + userId: userId, + user: { + notificationSetting: { + marketingEnabled: true + } + } + } + }) + + return subscriptions; +} + export const deletePushSubscription = async (id) => { await prisma.pushSubscription.delete({ where: {id: id} @@ -485,6 +500,7 @@ export const findReceivedLettersBySender = async ({ userId, senderUserId, letter title: true, content: true, deliveredAt: true, + readAt: true, createdAt: true, question: { select: { @@ -528,6 +544,7 @@ export const findSentLettersByReceiver = async ({ userId, receiverUserId, letter title: true, content: true, deliveredAt: true, + readAt: true, createdAt: true, question: { select: { @@ -639,6 +656,45 @@ export const findNoticeById = async (id) => { }); }; +export const findRecentNotices = async (minutes = 10) => { + const now = new Date(); + const minutesAgo = new Date(now.getTime() - minutes * 60 * 1000); + + return prisma.notice.findMany({ + where: { + createdAt: { + gte: minutesAgo, + lte: now, + }, + }, + orderBy: { createdAt: "desc" }, + select: { + id: true, + title: true, + createdAt: true, + }, + }); +}; + +export const findUsersWithMarketingPushEnabled = async () => { + const users = await prisma.user.findMany({ + where: { + isDeleted: false, + notificationSetting: { + marketingEnabled: true, + }, + pushSubscriptions: { + some: {}, + }, + }, + select: { + id: true, + }, + }); + + return users.map((user) => user.id); +}; + // ========== Notification Repository ========== /** * 알림 설정 조회 diff --git a/src/schemas/mailbox.schema.js b/src/schemas/mailbox.schema.js index 3a0bd2a..1a04560 100644 --- a/src/schemas/mailbox.schema.js +++ b/src/schemas/mailbox.schema.js @@ -1,8 +1,8 @@ import { z } from "zod"; // ========== Mailbox Schema ========== -export const threadIdParamSchema = z.object({ +export const sessionIdParamSchema = z.object({ params: z.object({ - threadId: z.coerce.number("threadId는 숫자여야 합니다.").int("threadId는 정수여야 합니다.").positive("threadId는 1부터 유효합니다.") + sessionId: z.coerce.number("sessionId는 숫자여야 합니다.").int("sessionId는 정수여야 합니다.").positive("sessionId는 1부터 유효합니다.") }) }); diff --git a/src/services/mailbox.service.js b/src/services/mailbox.service.js index 4c30854..1e96796 100644 --- a/src/services/mailbox.service.js +++ b/src/services/mailbox.service.js @@ -1,7 +1,5 @@ import { findReceivedLettersForThreads, - findReceivedLettersBySender, - findSentLettersByReceiver, findSelfLetters, findUsersNicknameByIds, } from "../repositories/user.repository.js"; @@ -9,6 +7,8 @@ import { LETTER_TYPE_ANON, LETTER_TYPE_SELF, makePreview } from "../utils/user.u import { findFriendById } from "../repositories/friend.repository.js"; import { NotFriendError } from "../errors/friend.error.js"; import { getFriendLetters, getMyLettersWithFriend } from "../repositories/letter.repository.js"; +import { prisma } from "../configs/db.config.js"; +import { MailboxInvalidSessionIdError } from "../errors/mailbox.error.js"; // ------------------------------ // Mailbox @@ -20,42 +20,99 @@ export const getAnonymousThreads = async (userId) => { letterType: LETTER_TYPE_ANON, }); - const latestBySender = new Map(); // senderUserId -> letter + // sessionId로 그룹화 + const latestBySession = new Map(); // sessionId -> { letter, otherParticipantId } for (const l of receivedLetters) { - if (!l.senderUserId) continue; - if (!latestBySender.has(l.senderUserId)) { - latestBySender.set(l.senderUserId, l); + if (!l.sessionId || !l.senderUserId) continue; + + // 상대방 ID는 senderUserId + const otherParticipantId = l.senderUserId; + + if (!latestBySession.has(l.sessionId)) { + latestBySession.set(l.sessionId, { + letter: l, + otherParticipantId: otherParticipantId + }); + } else { + // 더 최신 편지로 업데이트 + const existing = latestBySession.get(l.sessionId); + const existingTime = existing.letter.deliveredAt + ? new Date(existing.letter.deliveredAt).getTime() + : new Date(existing.letter.createdAt).getTime(); + const currentTime = l.deliveredAt + ? new Date(l.deliveredAt).getTime() + : new Date(l.createdAt).getTime(); + + if (currentTime > existingTime) { + latestBySession.set(l.sessionId, { + letter: l, + otherParticipantId: otherParticipantId + }); + } } } - const senderIds = Array.from(latestBySender.keys()); - const nicknameMap = senderIds.length ? await findUsersNicknameByIds(senderIds) : new Map(); + const sessionIds = Array.from(latestBySession.keys()); + const sessionData = Array.from(latestBySession.values()); + const otherParticipantIds = sessionData.map(s => s.otherParticipantId); + const nicknameMap = otherParticipantIds.length + ? await findUsersNicknameByIds(otherParticipantIds) + : new Map(); - const letters = senderIds.map((senderId) => { - const l = latestBySender.get(senderId); + // 각 세션별 편지 개수 조회 (sessionId 기준, 개별 조회) + const letterCounts = await Promise.all( + sessionIds.map(async (sessionId) => { + const count = await prisma.letter.count({ + where: { + letterType: LETTER_TYPE_ANON, + sessionId: sessionId, + OR: [ + { receiverUserId: userId }, + { senderUserId: userId } + ] + } + }); + return [sessionId, count]; + }) + ); + const letterCountMap = new Map(letterCounts); + + // 각 세션별 읽지 않은 편지 체크 (받은 편지만 체크) + const unreadChecks = await Promise.all( + sessionIds.map(async (sessionId) => { + const unreadCount = await prisma.letter.count({ + where: { + letterType: LETTER_TYPE_ANON, + sessionId: sessionId, + receiverUserId: userId, + readAt: null, // 읽지 않은 편지 + } + }); + return [sessionId, unreadCount > 0]; // 하나라도 있으면 true + }) + ); + const hasUnreadMap = new Map(unreadChecks); + + const letters = sessionIds.map((sessionId) => { + const { letter: l, otherParticipantId } = latestBySession.get(sessionId); return { - threadId: senderId, // threadId = senderUserId + sessionId: sessionId, // threadId -> sessionId로 변경 lastLetterId: l.id, lastLetterTitle: l.title, lastLetterPreview: makePreview(l.content, 30), deliveredAt: l.deliveredAt ?? null, + hasUnread: hasUnreadMap.get(sessionId) ?? false, // 읽지 않은 편지 여부 sender: { - id: senderId, - nickname: nicknameMap.get(senderId) ?? null, + id: otherParticipantId, // 상대방 ID + nickname: nicknameMap.get(otherParticipantId) ?? null, + letterCount: letterCountMap.get(sessionId) ?? 0, // sessionId 기준 개수 + }, + design: { + paperId: l.design?.paper?.id ?? null, + stampId: l.design?.stamp?.id ?? null, + stampUrl: l.design?.stamp?.assetUrl ?? null, }, - stampId: l.design?.stamp?.id ?? null, - stampUrl: l.design?.stamp?.assetUrl ?? null, - design: l.design - ? { - paper: l.design.paper - ? { - id: l.design.paper.id, - name: l.design.paper.color, // color를 name으로 매핑 - } - : null, - } - : { paper: null }, }; }); @@ -68,21 +125,108 @@ export const getAnonymousThreads = async (userId) => { return { letters }; }; -export const getAnonymousThreadLetters = async (userId, threadIdRaw) => { - const threadId = Number(threadIdRaw); +export const getAnonymousThreadLetters = async (userId, sessionIdRaw) => { + const sessionId = Number(sessionIdRaw); - // 받은 편지 조회 - const receivedLetters = await findReceivedLettersBySender({ - userId, - senderUserId: threadId, - letterType: LETTER_TYPE_ANON, + // 세션 참가자 조회하여 권한 확인 + const session = await prisma.matchingSession.findFirst({ + where: { + id: sessionId, + participants: { + some: { + userId: userId // 현재 사용자가 참가자인 세션만 + } + } + }, + include: { + participants: { + select: { + userId: true + } + } + } }); - // 보낸 편지 조회 - const sentLetters = await findSentLettersByReceiver({ - userId, - receiverUserId: threadId, - letterType: LETTER_TYPE_ANON, + if (!session) { + throw new MailboxInvalidSessionIdError(); + } + + // 받은 편지 조회 (sessionId로 필터링) + const receivedLetters = await prisma.letter.findMany({ + where: { + receiverUserId: userId, + sessionId: sessionId, + letterType: LETTER_TYPE_ANON, + }, + orderBy: [{ deliveredAt: "desc" }, { createdAt: "desc" }], + select: { + id: true, + title: true, + deliveredAt: true, + readAt: true, + createdAt: true, + question: { + select: { + content: true + } + }, + design: { + select: { + paper: { + select: { + id: true, + color: true, + } + }, + stamp: { + select: { + id: true, + name: true, + assetUrl: true + } + }, + }, + }, + }, + }); + + // 보낸 편지 조회 (sessionId로 필터링) + const sentLetters = await prisma.letter.findMany({ + where: { + senderUserId: userId, + sessionId: sessionId, + letterType: LETTER_TYPE_ANON, + }, + orderBy: [{ deliveredAt: "desc" }, { createdAt: "desc" }], + select: { + id: true, + title: true, + deliveredAt: true, + readAt: true, + createdAt: true, + question: { + select: { + content: true + } + }, + design: { + select: { + paper: { + select: { + id: true, + color: true, + } + }, + stamp: { + select: { + id: true, + name: true, + assetUrl: true + } + }, + }, + }, + }, }); // firstQuestion: 받은 편지의 첫 번째 질문 우선, 없으면 보낸 편지의 첫 번째 질문 @@ -93,19 +237,13 @@ export const getAnonymousThreadLetters = async (userId, threadIdRaw) => { id: l.id, title: l.title, deliveredAt: l.deliveredAt ?? null, + readAt: l.readAt ?? null, isMine: false, - stampId: l.design?.stamp?.id ?? null, - stampUrl: l.design?.stamp?.assetUrl ?? null, - design: l.design - ? { - paper: l.design.paper - ? { - id: l.design.paper.id, - name: l.design.paper.color, // color를 name으로 매핑 - } - : null, - } - : { paper: null }, + design: { + paperId: l.design?.paper?.id ?? null, + stampId: l.design?.stamp?.id ?? null, + stampUrl: l.design?.stamp?.assetUrl ?? null, + }, })); // 보낸 편지에 isMine: true 추가 @@ -113,19 +251,13 @@ export const getAnonymousThreadLetters = async (userId, threadIdRaw) => { id: l.id, title: l.title, deliveredAt: l.deliveredAt ?? null, + readAt: l.readAt ?? null, isMine: true, - stampId: l.design?.stamp?.id ?? null, - stampUrl: l.design?.stamp?.assetUrl ?? null, - design: l.design - ? { - paper: l.design.paper - ? { - id: l.design.paper.id, - name: l.design.paper.color, // color를 name으로 매핑 - } - : null, - } - : { paper: null }, + design: { + paperId: l.design?.paper?.id ?? null, + stampId: l.design?.stamp?.id ?? null, + stampUrl: l.design?.stamp?.assetUrl ?? null, + }, })); // 받은 편지 먼저, 보낸 편지 나중에 배치 diff --git a/src/services/notice.service.js b/src/services/notice.service.js index 466948c..f2d795d 100644 --- a/src/services/notice.service.js +++ b/src/services/notice.service.js @@ -19,3 +19,7 @@ export const getNoticeDetail = async (noticeId) => { return notice; }; + +// sendNoticePushNotification 함수는 삭제됨 +// jobs/bootstraps/push.bootstrap.js의 sendNoticePushNotifications로 대체됨 +// 비동기 메시지 큐(BullMQ)를 통해 처리됩니다. diff --git a/src/services/push.service.js b/src/services/push.service.js index 60bfee7..1a0a9f6 100644 --- a/src/services/push.service.js +++ b/src/services/push.service.js @@ -1,9 +1,11 @@ import { NOTIFICATION_MESSAGES } from "../constants/push.constant.js"; -import { deletePushSubscription, getPushSubscription } from "../repositories/user.repository.js" +import { deletePushSubscription, getPushSubscription, getPushSubscriptionForMarketing } from "../repositories/user.repository.js" import webpush from "web-push" -export const sendPushNotification = async ({userId, type, data = {}}) => { - const subscriptions = await getPushSubscription(userId); +export const sendPushNotification = async ({userId, type, data = {}, useMarketing = false}) => { + const subscriptions = useMarketing + ? await getPushSubscriptionForMarketing(userId) + : await getPushSubscription(userId); if(subscriptions.length === 0) return; const messageConfig = NOTIFICATION_MESSAGES[type]; diff --git a/src/swagger/mailbox.swagger.js b/src/swagger/mailbox.swagger.js index e74fbf0..44a1ee3 100644 --- a/src/swagger/mailbox.swagger.js +++ b/src/swagger/mailbox.swagger.js @@ -3,7 +3,7 @@ * /mailbox/anonymous: * get: * summary: 익명 탭 목록 조회 - * description: 익명으로 받은 편지의 스레드 목록을 조회합니다. senderUserId별 최신 편지 1개씩 반환됩니다. + * description: 익명으로 받은 편지의 스레드 목록을 조회합니다. sessionId별 최신 편지 1개씩 반환됩니다. * tags: [편지함] * security: * - bearerAuth: [] @@ -24,9 +24,9 @@ * items: * type: object * properties: - * threadId: + * sessionId: * type: integer - * description: 스레드 ID (senderUserId) + * description: 세션 ID (MatchingSession ID) * sender: * type: object * properties: @@ -35,6 +35,9 @@ * nickname: * type: string * nullable: true + * letterCount: + * type: integer + * description: 해당 세션의 편지 개수 * lastLetterId: * type: integer * lastLetterTitle: @@ -45,26 +48,22 @@ * type: string * format: date-time * nullable: true - * stampId: - * type: integer - * nullable: true - * stampUrl: - * type: string - * nullable: true - * description: 스탬프 이미지 URL + * hasUnread: + * type: boolean + * description: 해당 세션에 읽지 않은 편지가 있는지 여부 (받은 편지 기준) * design: * type: object * properties: - * paper: - * type: object + * paperId: + * type: integer * nullable: true - * properties: - * id: - * type: integer - * name: - * type: string - * assetUrl: - * type: string + * stampId: + * type: integer + * nullable: true + * stampUrl: + * type: string + * nullable: true + * description: 스탬프 이미지 URL * 401: * description: | * 인증 실패: @@ -152,7 +151,7 @@ /** * @swagger - * /mailbox/anonymous/threads/{threadId}/letters: + * /mailbox/anonymous/threads/{sessionId}/letters: * get: * summary: 익명 스레드 편지 목록 조회 * description: 특정 익명 스레드의 편지 목록을 조회합니다. @@ -161,11 +160,11 @@ * - bearerAuth: [] * parameters: * - in: path - * name: threadId + * name: sessionId * required: true * schema: * type: integer - * description: 스레드 ID (senderUserId) + * description: 세션 ID (MatchingSession ID) * responses: * 200: * description: 성공 @@ -195,32 +194,31 @@ * type: string * format: date-time * nullable: true + * readAt: + * type: string + * format: date-time + * nullable: true * isMine: * type: boolean * description: 내가 보낸 편지인지 여부 - * stampId: - * type: integer - * nullable: true - * stampUrl: - * type: string - * nullable: true - * description: 스탬프 이미지 URL * design: * type: object * properties: - * paper: - * type: object + * paperId: + * type: integer * nullable: true - * properties: - * id: - * type: integer - * name: - * type: string + * stampId: + * type: integer + * nullable: true + * stampUrl: + * type: string + * nullable: true + * description: 스탬프 이미지 URL * 400: * description: | * 잘못된 요청: * - `REQ_BAD_REQUEST`: 요청 유효성 검사 실패 - * - `MAILBOX_INVALID_THREAD_ID`: threadId가 올바르지 않습니다. + * - `MAILBOX_INVALID_SESSION_ID`: sessionId가 올바르지 않습니다. * content: * application/json: * schema: @@ -240,9 +238,9 @@ * error: * properties: * errorCode: - * example: "MAILBOX_INVALID_THREAD_ID" + * example: "MAILBOX_INVALID_SESSION_ID" * reason: - * example: "threadId가 올바르지 않습니다." + * example: "sessionId가 올바르지 않습니다." * 401: * description: | * 인증 실패: