From 993c3a064e30b44f99774d5118aed5076d72e8a6 Mon Sep 17 00:00:00 2001 From: Jaewoong Choi Date: Sun, 31 May 2026 18:56:23 +0900 Subject: [PATCH 1/4] feat: add isSaved field to Club model and schema Co-Authored-By: Claude Opus 4.7 --- server/domain/model/Club.ts | 3 +++ src/lib/schemas/common.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/server/domain/model/Club.ts b/server/domain/model/Club.ts index f417154..51db8c2 100644 --- a/server/domain/model/Club.ts +++ b/server/domain/model/Club.ts @@ -60,6 +60,7 @@ export type Club = { totalReviews: number reviewKeywords: ReviewKeyword[] latestComment: string + isSaved: boolean } export const toClubDomain = ( @@ -70,6 +71,7 @@ export const toClubDomain = ( reviewKeywords: ReviewKeyword[] latestComment: string }, + isSaved?: boolean, ): Club => ({ id: it.uuid, uuid: it.uuid, @@ -112,6 +114,7 @@ export const toClubDomain = ( totalReviews: review?.totalReviews ?? 0, reviewKeywords: review?.reviewKeywords ?? [], latestComment: review?.latestComment ?? '', + isSaved: isSaved ?? false, }) function encode(imageUri: string | undefined): string { diff --git a/src/lib/schemas/common.ts b/src/lib/schemas/common.ts index 9e85c05..28572e5 100644 --- a/src/lib/schemas/common.ts +++ b/src/lib/schemas/common.ts @@ -143,6 +143,7 @@ export const ClubSchema = z totalReviews: z.number().int(), reviewKeywords: z.array(ReviewKeywordSchema), latestComment: z.string(), + isSaved: z.boolean(), }) .openapi('Club') From dc6bb0871475c186b4e3374290dbccb654896c82 Mon Sep 17 00:00:00 2001 From: Jaewoong Choi Date: Sun, 31 May 2026 18:56:33 +0900 Subject: [PATCH 2/4] feat: populate isSaved for the requesting user in club and search services Add getSavedClubIdSet helper and thread an optional serviceUserId through club lookups and search hydration so each returned club reflects whether the current user has saved it. Co-Authored-By: Claude Opus 4.7 --- server/service/club.service.ts | 73 ++++++++++++++----- server/service/search.service.ts | 13 +++- .../search/search-result-hydrator.service.ts | 10 ++- 3 files changed, 70 insertions(+), 26 deletions(-) diff --git a/server/service/club.service.ts b/server/service/club.service.ts index aa60cbf..bd454db 100644 --- a/server/service/club.service.ts +++ b/server/service/club.service.ts @@ -71,7 +71,7 @@ export class ClubService { @Inject(ClubAccessService) private readonly clubAccessService: ClubAccessService - async findByUuid(uuid: string): Promise { + async findByUuid(uuid: string, serviceUserId: string | null = null): Promise { this.userActivityLogRepository .insert({ type: UserActivityLogType.CALL_GET_CLUB_API, @@ -79,11 +79,14 @@ export class ClubService { }) .catch(console.error) const club = await this.clubAccessService.getExistingClub(uuid) - const clubReview = await this.getClubReviews([club.uuid]) - return toClubDomain(club, clubReview.get(club.uuid)) + const [clubReview, savedSet] = await Promise.all([ + this.getClubReviews([club.uuid]), + this.getSavedClubIdSet(serviceUserId, [club.uuid]), + ]) + return toClubDomain(club, clubReview.get(club.uuid), savedSet.has(club.uuid)) } - async findPublicByUuid(uuid: string): Promise { + async findPublicByUuid(uuid: string, serviceUserId: string | null = null): Promise { this.userActivityLogRepository .insert({ type: UserActivityLogType.CALL_GET_CLUB_API, @@ -91,8 +94,11 @@ export class ClubService { }) .catch(console.error) const club = await this.clubAccessService.getPublicClub(uuid) - const clubReview = await this.getClubReviews([club.uuid]) - return toClubDomain(club, clubReview.get(club.uuid)) + const [clubReview, savedSet] = await Promise.all([ + this.getClubReviews([club.uuid]), + this.getSavedClubIdSet(serviceUserId, [club.uuid]), + ]) + return toClubDomain(club, clubReview.get(club.uuid), savedSet.has(club.uuid)) } async findByAuthKey(authkey: string): Promise { @@ -120,7 +126,7 @@ export class ClubService { } } - async findByCategory(category: string): Promise { + async findByCategory(category: string, serviceUserId: string | null = null): Promise { this.userActivityLogRepository .insert({ type: UserActivityLogType.CALL_LIST_CLUBS_OF_CATEGORY_API, @@ -132,8 +138,14 @@ export class ClubService { status: PUBLIC_CLUB_STATUS, deletedAt: IsNull(), }) - const clubReview = await this.getClubReviews(entities.map((it) => it.uuid)) - const clubs = entities.map((it) => toClubDomain(it, clubReview.get(it.uuid))) + const uuids = entities.map((it) => it.uuid) + const [clubReview, savedSet] = await Promise.all([ + this.getClubReviews(uuids), + this.getSavedClubIdSet(serviceUserId, uuids), + ]) + const clubs = entities.map((it) => + toClubDomain(it, clubReview.get(it.uuid), savedSet.has(it.uuid)), + ) return sortByPopularAndEachRandom(clubs) } @@ -159,6 +171,15 @@ export class ClubService { return club.map((it) => toClubDomain(it)) } + async getSavedClubIdSet(serviceUserId: string | null, clubUuids: string[]): Promise> { + if (!serviceUserId || clubUuids.length === 0) return new Set() + const saved = await this.userSavedClubRepository.find({ + where: { serviceUserId, clubId: In(clubUuids) }, + select: ['clubId'], + }) + return new Set(saved.map((it) => it.clubId)) + } + async findMySavedClubs(serviceUserId: string): Promise { const savedClubs = await this.userSavedClubRepository.findBy({ serviceUserId }) const clubIds = Array.from(new Set(savedClubs.map((it) => it.clubId))) @@ -167,7 +188,7 @@ export class ClubService { status: PUBLIC_CLUB_STATUS, deletedAt: IsNull(), }) - return club.map((it) => toClubDomain(it)) + return club.map((it) => toClubDomain(it, undefined, true)) } async saveClubToMyCollection(serviceUserId: string, clubId: string) { @@ -179,7 +200,7 @@ export class ClubService { await this.userSavedClubRepository.delete({ serviceUserId, clubId }) } - async findPopular(): Promise { + async findPopular(serviceUserId: string | null = null): Promise { this.userActivityLogRepository .insert({ type: UserActivityLogType.CALL_LIST_POPULAR_CLUBS_API, @@ -191,12 +212,18 @@ export class ClubService { status: PUBLIC_CLUB_STATUS, deletedAt: IsNull(), }) - const clubReview = await this.getClubReviews(entities.map((it) => it.uuid)) - const clubs = entities.map((it) => toClubDomain(it, clubReview.get(it.uuid))) + const uuids = entities.map((it) => it.uuid) + const [clubReview, savedSet] = await Promise.all([ + this.getClubReviews(uuids), + this.getSavedClubIdSet(serviceUserId, uuids), + ]) + const clubs = entities.map((it) => + toClubDomain(it, clubReview.get(it.uuid), savedSet.has(it.uuid)), + ) return sortByPopularAndEachRandom(clubs) } - async findLatestUploaded(topN = 20): Promise { + async findLatestUploaded(topN = 20, serviceUserId: string | null = null): Promise { const entities = await this.clubRepository.find({ where: { status: PUBLIC_CLUB_STATUS, @@ -210,11 +237,15 @@ export class ClubService { }, take: topN, }) - const clubReview = await this.getClubReviews(entities.map((it) => it.uuid)) - return entities.map((it) => toClubDomain(it, clubReview.get(it.uuid))) + const uuids = entities.map((it) => it.uuid) + const [clubReview, savedSet] = await Promise.all([ + this.getClubReviews(uuids), + this.getSavedClubIdSet(serviceUserId, uuids), + ]) + return entities.map((it) => toClubDomain(it, clubReview.get(it.uuid), savedSet.has(it.uuid))) } - async findRandomRecommendations(limit = 5): Promise { + async findRandomRecommendations(limit = 5, serviceUserId: string | null = null): Promise { const entities = await this.clubRepository .createQueryBuilder('club') .where('club.status = :status', { status: PUBLIC_CLUB_STATUS }) @@ -223,8 +254,12 @@ export class ClubService { .take(limit) .getMany() - const clubReview = await this.getClubReviews(entities.map((it) => it.uuid)) - return entities.map((it) => toClubDomain(it, clubReview.get(it.uuid))) + const uuids = entities.map((it) => it.uuid) + const [clubReview, savedSet] = await Promise.all([ + this.getClubReviews(uuids), + this.getSavedClubIdSet(serviceUserId, uuids), + ]) + return entities.map((it) => toClubDomain(it, clubReview.get(it.uuid), savedSet.has(it.uuid))) } async registerClub(serviceUserId: string, body: ClubRegisterRequest): Promise { diff --git a/server/service/search.service.ts b/server/service/search.service.ts index ca83d5e..62efa4c 100644 --- a/server/service/search.service.ts +++ b/server/service/search.service.ts @@ -32,10 +32,11 @@ export class SearchService { async searchWithTypoCorrection( query: string, options: SearchOptions = { filters: {} }, + serviceUserId: string | null = null, ): Promise { this.searchLogService.logSearch(query) - const clubs = await this.runSearch(query, options) + const clubs = await this.runSearch(query, options, serviceUserId) if (clubs.length > 0) { return { clubs, @@ -46,7 +47,7 @@ export class SearchService { const correctedQuery = await this.typoCorrectionService.findCorrectedQuery(query) if (correctedQuery && correctedQuery !== query) { - const correctedClubs = await this.runSearch(correctedQuery, options) + const correctedClubs = await this.runSearch(correctedQuery, options, serviceUserId) if (correctedClubs.length > 0) { return { clubs: correctedClubs, @@ -63,9 +64,13 @@ export class SearchService { } } - private async runSearch(query: string, options: SearchOptions): Promise { + private async runSearch( + query: string, + options: SearchOptions, + serviceUserId: string | null = null, + ): Promise { const entities: ClubEntity[] = await this.searchQueryService.search(query, options.filters) - const clubs = await this.hydratorService.toClubs(entities) + const clubs = await this.hydratorService.toClubs(entities, serviceUserId) return this.sortService.sort(clubs, query, options.sort ?? DEFAULT_SEARCH_SORT) } } diff --git a/server/service/search/search-result-hydrator.service.ts b/server/service/search/search-result-hydrator.service.ts index f35dfc1..afa2ac6 100644 --- a/server/service/search/search-result-hydrator.service.ts +++ b/server/service/search/search-result-hydrator.service.ts @@ -8,13 +8,17 @@ export class SearchResultHydratorService { @Inject(ClubService) private readonly clubService: ClubService - async toClubs(entities: ClubEntity[]): Promise { + async toClubs(entities: ClubEntity[], serviceUserId: string | null = null): Promise { const uniqueEntities = this.dedupe(entities) if (uniqueEntities.length === 0) { return [] } - const reviews = await this.clubService.getClubReviews(uniqueEntities.map((it) => it.uuid)) - return uniqueEntities.map((it) => toClubDomain(it, reviews.get(it.uuid))) + const uuids = uniqueEntities.map((it) => it.uuid) + const [reviews, savedSet] = await Promise.all([ + this.clubService.getClubReviews(uuids), + this.clubService.getSavedClubIdSet(serviceUserId, uuids), + ]) + return uniqueEntities.map((it) => toClubDomain(it, reviews.get(it.uuid), savedSet.has(it.uuid))) } private dedupe(entities: ClubEntity[]): ClubEntity[] { From e490bd1b3956123c0b730ef7cd3bdeaa8f64587b Mon Sep 17 00:00:00 2001 From: Jaewoong Choi Date: Sun, 31 May 2026 18:56:37 +0900 Subject: [PATCH 3/4] feat: resolve optional auth on club list, detail, and search endpoints Resolve the optional caller identity and pass the resolved serviceUserId to the club and search services so list, detail, and search responses include the isSaved flag for authenticated users. Co-Authored-By: Claude Opus 4.7 --- pages/api/v2/clubs/[uuid]/index.ts | 21 +++++++++++++++++-- pages/api/v2/clubs/index.ts | 20 +++++++++++++++++- pages/api/v2/clubs/latest/index.ts | 20 +++++++++++++++++- pages/api/v2/clubs/popular/index.ts | 20 +++++++++++++++++- .../v2/clubs/recommendations/random/index.ts | 20 +++++++++++++++++- pages/api/v2/clubs/search/index.ts | 11 +++++++++- 6 files changed, 105 insertions(+), 7 deletions(-) diff --git a/pages/api/v2/clubs/[uuid]/index.ts b/pages/api/v2/clubs/[uuid]/index.ts index e1c69cd..db98dad 100644 --- a/pages/api/v2/clubs/[uuid]/index.ts +++ b/pages/api/v2/clubs/[uuid]/index.ts @@ -1,10 +1,12 @@ import { NextApiRequest, NextApiResponse } from 'next' import { Provider } from 'server/provider' import { ClubService } from 'server/service/club.service' +import { UserService } from 'server/service/user.service' import { Club } from 'server/domain/model/Club' import { z, ZodIssue } from 'zod' import { ClubUuidParamsSchema } from 'src/lib/schemas/clubs' -import { NotFoundError } from 'server/domain/error' +import { BadRequestError, NotFoundError, UnauthorizedError } from 'server/domain/error' +import { resolveOptionalAuth } from 'server/util/optional-auth' export default async function handler( req: NextApiRequest, @@ -12,15 +14,30 @@ export default async function handler( ) { try { const clubService = Provider.getService(ClubService) + const userService = Provider.getService(UserService) if (req.method == 'GET') { const { uuid: ClubUuid } = ClubUuidParamsSchema.parse(req.query) - const club = await clubService.findPublicByUuid(ClubUuid) + const auth = await resolveOptionalAuth(req) + const serviceUserId = + auth.type === 'member' + ? await userService + .getUserByAccountId(auth.accountId) + .then((u) => u.serviceUserId) + .catch(() => null) + : null + const club = await clubService.findPublicByUuid(ClubUuid, serviceUserId) return res.status(200).json(club) } } catch (err) { if (err instanceof NotFoundError) { return res.status(404).send('club not found') } + if (err instanceof UnauthorizedError) { + return res.status(401).send('unauthorized') + } + if (err instanceof BadRequestError) { + return res.status(400).send(err.message) + } if (err instanceof z.ZodError) { return res.status(400).json(err.errors) } diff --git a/pages/api/v2/clubs/index.ts b/pages/api/v2/clubs/index.ts index a860d2c..384bfc0 100644 --- a/pages/api/v2/clubs/index.ts +++ b/pages/api/v2/clubs/index.ts @@ -1,7 +1,10 @@ import { NextApiRequest, NextApiResponse } from 'next' import { Provider } from 'server/provider' import { ClubService } from 'server/service/club.service' +import { UserService } from 'server/service/user.service' import { Club } from 'server/domain/model/Club' +import { resolveOptionalAuth } from 'server/util/optional-auth' +import { BadRequestError, UnauthorizedError } from 'server/domain/error' type ResponseData = { clubs: Club[] @@ -14,19 +17,34 @@ export default async function handler( ) { try { const clubService = Provider.getService(ClubService) + const userService = Provider.getService(UserService) if (req.method == 'GET') { const category = req.query.category as string if (!category) { return res.status(400).send('category is required') } - const clubs = await clubService.findByCategory(category) + const auth = await resolveOptionalAuth(req) + const serviceUserId = + auth.type === 'member' + ? await userService + .getUserByAccountId(auth.accountId) + .then((u) => u.serviceUserId) + .catch(() => null) + : null + const clubs = await clubService.findByCategory(category, serviceUserId) return res.status(200).json({ clubs: clubs, totalSize: clubs.length, }) } } catch (err) { + if (err instanceof UnauthorizedError) { + return res.status(401).send('unauthorized') + } + if (err instanceof BadRequestError) { + return res.status(400).send(err.message) + } console.error('listClubs error: ', err) return res.status(500).send('Internal Server Error') } diff --git a/pages/api/v2/clubs/latest/index.ts b/pages/api/v2/clubs/latest/index.ts index 6a9a4a6..5bbccee 100644 --- a/pages/api/v2/clubs/latest/index.ts +++ b/pages/api/v2/clubs/latest/index.ts @@ -1,7 +1,10 @@ import { NextApiRequest, NextApiResponse } from 'next' import { Provider } from 'server/provider' import { ClubService } from 'server/service/club.service' +import { UserService } from 'server/service/user.service' import { Club } from 'server/domain/model/Club' +import { resolveOptionalAuth } from 'server/util/optional-auth' +import { BadRequestError, UnauthorizedError } from 'server/domain/error' type ResponseData = { clubs: Club[] @@ -14,15 +17,30 @@ export default async function handler( ) { try { const clubService = Provider.getService(ClubService) + const userService = Provider.getService(UserService) if (req.method == 'GET') { - const clubs = await clubService.findLatestUploaded() + const auth = await resolveOptionalAuth(req) + const serviceUserId = + auth.type === 'member' + ? await userService + .getUserByAccountId(auth.accountId) + .then((u) => u.serviceUserId) + .catch(() => null) + : null + const clubs = await clubService.findLatestUploaded(undefined, serviceUserId) return res.status(200).json({ clubs: clubs, totalSize: clubs.length, }) } } catch (err) { + if (err instanceof UnauthorizedError) { + return res.status(401).send('unauthorized') + } + if (err instanceof BadRequestError) { + return res.status(400).send(err.message) + } console.error('listLatestClubs error: ', err) return res.status(500).send('Internal Server Error') } diff --git a/pages/api/v2/clubs/popular/index.ts b/pages/api/v2/clubs/popular/index.ts index 2056356..725ccb7 100644 --- a/pages/api/v2/clubs/popular/index.ts +++ b/pages/api/v2/clubs/popular/index.ts @@ -1,7 +1,10 @@ import { NextApiRequest, NextApiResponse } from 'next' import { Provider } from 'server/provider' import { ClubService } from 'server/service/club.service' +import { UserService } from 'server/service/user.service' import { Club } from 'server/domain/model/Club' +import { resolveOptionalAuth } from 'server/util/optional-auth' +import { BadRequestError, UnauthorizedError } from 'server/domain/error' type ResponseData = { clubs: Club[] @@ -14,15 +17,30 @@ export default async function handler( ) { try { const clubService = Provider.getService(ClubService) + const userService = Provider.getService(UserService) if (req.method == 'GET') { - const clubs = await clubService.findPopular() + const auth = await resolveOptionalAuth(req) + const serviceUserId = + auth.type === 'member' + ? await userService + .getUserByAccountId(auth.accountId) + .then((u) => u.serviceUserId) + .catch(() => null) + : null + const clubs = await clubService.findPopular(serviceUserId) return res.status(200).json({ clubs: clubs, totalSize: clubs.length, }) } } catch (err) { + if (err instanceof UnauthorizedError) { + return res.status(401).send('unauthorized') + } + if (err instanceof BadRequestError) { + return res.status(400).send(err.message) + } console.error('listPopularClubs error: ', err) return res.status(500).send('Internal Server Error') } diff --git a/pages/api/v2/clubs/recommendations/random/index.ts b/pages/api/v2/clubs/recommendations/random/index.ts index 4149a71..73c253d 100644 --- a/pages/api/v2/clubs/recommendations/random/index.ts +++ b/pages/api/v2/clubs/recommendations/random/index.ts @@ -1,7 +1,10 @@ import { NextApiRequest, NextApiResponse } from 'next' import { Provider } from 'server/provider' import { ClubService } from 'server/service/club.service' +import { UserService } from 'server/service/user.service' import { Club } from 'server/domain/model/Club' +import { resolveOptionalAuth } from 'server/util/optional-auth' +import { BadRequestError, UnauthorizedError } from 'server/domain/error' type ResponseData = { clubs: Club[] @@ -14,15 +17,30 @@ export default async function handler( ) { try { const clubService = Provider.getService(ClubService) + const userService = Provider.getService(UserService) if (req.method == 'GET') { - const clubs = await clubService.findRandomRecommendations(5) + const auth = await resolveOptionalAuth(req) + const serviceUserId = + auth.type === 'member' + ? await userService + .getUserByAccountId(auth.accountId) + .then((u) => u.serviceUserId) + .catch(() => null) + : null + const clubs = await clubService.findRandomRecommendations(5, serviceUserId) return res.status(200).json({ clubs: clubs, totalSize: clubs.length, }) } } catch (err) { + if (err instanceof UnauthorizedError) { + return res.status(401).send('unauthorized') + } + if (err instanceof BadRequestError) { + return res.status(400).send(err.message) + } console.error('listRandomRecommendedClubs error: ', err) return res.status(500).send('Internal Server Error') } diff --git a/pages/api/v2/clubs/search/index.ts b/pages/api/v2/clubs/search/index.ts index 3067ba3..9076cb7 100644 --- a/pages/api/v2/clubs/search/index.ts +++ b/pages/api/v2/clubs/search/index.ts @@ -1,6 +1,7 @@ import { NextApiRequest, NextApiResponse } from 'next' import { Provider } from 'server/provider' import { SearchService } from 'server/service/search.service' +import { UserService } from 'server/service/user.service' import { Club } from 'server/domain/model/Club' import { MinActivityPeriodFilter, SearchFilters } from 'server/service/search/search.types' import { BadRequestError, UnauthorizedError } from 'server/domain/error' @@ -30,6 +31,14 @@ export default async function handler( if (req.method == 'GET') { const auth = await resolveOptionalAuth(req) + const userService = Provider.getService(UserService) + const serviceUserId = + auth.type === 'member' + ? await userService + .getUserByAccountId(auth.accountId) + .then((u) => u.serviceUserId) + .catch(() => null) + : null const query = req.query.query as string if (!query) { return res.status(400).send('query is required') @@ -46,7 +55,7 @@ export default async function handler( } const { clubs, correctedQuery, isTypoCorrected } = - await searchService.searchWithTypoCorrection(query, { filters }) + await searchService.searchWithTypoCorrection(query, { filters }, serviceUserId) await saveRecentSearchBestEffort(auth, query) return res.status(200).json({ clubs: clubs, From fef0d726f9201f0cbbdae6814385ef4132326e0b Mon Sep 17 00:00:00 2001 From: Jaewoong Choi Date: Sun, 31 May 2026 18:56:40 +0900 Subject: [PATCH 4/4] docs: document optional auth and guest id header on club endpoints Co-Authored-By: Claude Opus 4.7 --- src/lib/openapi/register-paths.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/lib/openapi/register-paths.ts b/src/lib/openapi/register-paths.ts index a9c1135..dfc3e06 100644 --- a/src/lib/openapi/register-paths.ts +++ b/src/lib/openapi/register-paths.ts @@ -754,8 +754,10 @@ registry.registerPath({ path: '/api/v2/clubs', tags: ['Clubs'], summary: '카테고리별 동아리 목록', + security: [{ bearerAuth: [] }, { guestIdAuth: [] }], request: { query: ClubListByCategoryQuerySchema, + headers: GuestIdHeaderSchema, }, responses: { 200: { @@ -767,7 +769,7 @@ registry.registerPath({ }, }, 400: { - description: 'category 쿼리가 필요합니다.', + description: 'category 쿼리가 필요하거나 비회원 x-guest-id header가 잘못되었습니다.', content: { 'text/plain': { schema: ErrorMessageSchema, @@ -783,6 +785,10 @@ registry.registerPath({ path: '/api/v2/clubs/latest', tags: ['Clubs'], summary: '최신 등록 동아리 목록', + security: [{ bearerAuth: [] }, { guestIdAuth: [] }], + request: { + headers: GuestIdHeaderSchema, + }, responses: { 200: { description: '조회 성공', @@ -792,6 +798,10 @@ registry.registerPath({ }, }, }, + 400: { + description: '비회원 x-guest-id header가 잘못되었습니다.', + content: { 'text/plain': { schema: ErrorMessageSchema } }, + }, 500: internalServerErrorResponse, }, }) @@ -801,6 +811,10 @@ registry.registerPath({ path: '/api/v2/clubs/popular', tags: ['Clubs'], summary: '인기 동아리 목록', + security: [{ bearerAuth: [] }, { guestIdAuth: [] }], + request: { + headers: GuestIdHeaderSchema, + }, responses: { 200: { description: '조회 성공', @@ -810,6 +824,10 @@ registry.registerPath({ }, }, }, + 400: { + description: '비회원 x-guest-id header가 잘못되었습니다.', + content: { 'text/plain': { schema: ErrorMessageSchema } }, + }, 500: internalServerErrorResponse, }, }) @@ -820,6 +838,10 @@ registry.registerPath({ tags: ['Clubs'], summary: '랜덤 추천 동아리 목록', description: '검색 결과가 없을 때 노출할 공개 상태 동아리를 랜덤으로 최대 5개 추천합니다.', + security: [{ bearerAuth: [] }, { guestIdAuth: [] }], + request: { + headers: GuestIdHeaderSchema, + }, responses: { 200: { description: '조회 성공', @@ -829,6 +851,10 @@ registry.registerPath({ }, }, }, + 400: { + description: '비회원 x-guest-id header가 잘못되었습니다.', + content: { 'text/plain': { schema: ErrorMessageSchema } }, + }, 500: internalServerErrorResponse, }, }) @@ -929,8 +955,10 @@ registry.registerPath({ path: '/api/v2/clubs/{uuid}', tags: ['Clubs'], summary: '동아리 상세 조회', + security: [{ bearerAuth: [] }, { guestIdAuth: [] }], request: { params: ClubUuidParamsSchema, + headers: GuestIdHeaderSchema, }, responses: { 200: {